feat(chat): màn giới thiệu khi hội thoại còn rỗng
Bấm "Cuộc trò chuyện mới" trước đây để lại một khung trắng: không có gì nói người dùng đang làm trong project nào, thư mục có bao nhiêu tệp, hay bắt đầu từ đâu. Đây là trạng thái RỖNG — một trong bốn trạng thái mà mọi khung dữ liệu phải có, và là trạng thái duy nhất người dùng nhìn thấy trước khi gõ chữ đầu tiên. Gồm lời chào theo tên, dòng bối cảnh (project · số tệp · số skill đang bật), và bốn thẻ gợi ý. Hai quyết định: - Thẻ ĐIỀN câu gợi ý vào ô nhập chứ không gửi luôn. Câu gợi ý là điểm bắt đầu; người dùng gần như luôn cần thêm chi tiết của riêng họ, và gửi ngay sẽ tiêu một lượt gọi model cho một câu hỏi chung chung. - Dấu phía trên lời chào không bấm được — nó là dấu hiệu thị giác. Một nút không làm gì tệ hơn không có nút. Dòng bối cảnh phân biệt KHÔNG BIẾT với 0: đếm được 0 tệp thì hiện "0 tệp", còn không đọc được thư mục thì bỏ hẳn mảnh đó — hiện "0 tệp" khi người dùng vừa thấy có tệp trong thư mục còn tệ hơn là thiếu một mảnh. show_welcome() được móc ở ba chỗ: new_session(), load_conversation() (theo số tin nhắn đã lưu), và TRƯỚC mọi nhánh add_user trong turn runner — đặt sau từng add_user() thì dễ sót đúng một nhánh, và nhánh đó sẽ hiện cả hai thứ cùng lúc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
thanhnv
co-authored by
Claude Opus 5
parent
1a1f23030c
commit
77745ca4e0
@@ -0,0 +1,143 @@
|
||||
"""Màn giới thiệu của khung chat khi hội thoại còn rỗng.
|
||||
|
||||
Bấm "Cuộc trò chuyện mới" trước đây để lại một khung trắng: không có gì nói
|
||||
người dùng đang làm trong project nào, thư mục có bao nhiêu tệp, hay bắt đầu từ
|
||||
đâu. Đây là trạng thái RỖNG — trạng thái duy nhất người dùng nhìn thấy trước khi
|
||||
họ gõ chữ đầu tiên.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PySide6", reason="cần PySide6 để dựng widget thật")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def welcome(qapp):
|
||||
from cowork_local.presentation.chat.chat_welcome import ChatWelcome
|
||||
w = ChatWelcome()
|
||||
yield w
|
||||
w.deleteLater()
|
||||
|
||||
|
||||
# ---- bốn thẻ gợi ý -------------------------------------------------------
|
||||
|
||||
def test_co_dung_bon_the(welcome):
|
||||
assert len(welcome.cards) == 4
|
||||
|
||||
|
||||
def test_bam_the_thi_phat_cau_goi_y(welcome):
|
||||
nhan = []
|
||||
welcome.suggestion_picked.connect(nhan.append)
|
||||
|
||||
welcome.cards[0].click()
|
||||
|
||||
assert len(nhan) == 1
|
||||
assert len(nhan[0]) > 20, "câu gợi ý phải là một câu thật, không phải nhãn thẻ"
|
||||
|
||||
|
||||
def test_bon_the_cho_bon_cau_khac_nhau(welcome):
|
||||
nhan = []
|
||||
welcome.suggestion_picked.connect(nhan.append)
|
||||
|
||||
for card in welcome.cards:
|
||||
card.click()
|
||||
|
||||
assert len(set(nhan)) == 4
|
||||
|
||||
|
||||
def test_the_co_tieu_de_va_mo_ta(welcome):
|
||||
for card in welcome.cards:
|
||||
assert card.title_label.text().strip()
|
||||
assert card.sub_label.text().strip()
|
||||
# Nhãn của chính QPushButton phải rỗng — chữ do hai QLabel bên trong vẽ,
|
||||
# đặt cả hai chỗ sẽ in đè lên nhau.
|
||||
assert card.text() == ""
|
||||
|
||||
|
||||
# ---- dòng bối cảnh: KHÔNG BIẾT khác 0 ------------------------------------
|
||||
|
||||
def test_khong_biet_so_tep_thi_bo_manh_do(welcome):
|
||||
"""Hiện "0 tệp" khi người dùng vừa thấy có tệp trong thư mục còn tệ hơn là
|
||||
bỏ mảnh đó khỏi dòng meta."""
|
||||
welcome.refresh(user_name="local", project="p", files=-1, skills=-1)
|
||||
|
||||
meta = welcome.meta_label.text()
|
||||
assert "p" in meta
|
||||
assert "0" not in meta
|
||||
|
||||
|
||||
def test_khong_co_tep_that_thi_van_hien_so_0(welcome):
|
||||
"""Khác với KHÔNG BIẾT: thư mục rỗng thật thì nói rõ là rỗng."""
|
||||
welcome.refresh(user_name="local", project="p", files=0, skills=0)
|
||||
|
||||
assert "0" in welcome.meta_label.text()
|
||||
|
||||
|
||||
def test_khong_co_gi_de_noi_thi_an_dong_meta(welcome):
|
||||
welcome.refresh()
|
||||
|
||||
assert welcome.meta_label.isHidden() is True
|
||||
|
||||
|
||||
def test_co_ten_thi_chao_ten(welcome):
|
||||
welcome.refresh(user_name="Mynt4")
|
||||
|
||||
assert "Mynt4" in welcome.greeting_label.text()
|
||||
|
||||
|
||||
def test_khong_co_ten_thi_khong_chao_rong(welcome):
|
||||
"""Không được ra "Chào — bắt đầu từ đâu?" với một khoảng trống."""
|
||||
from cowork_local.i18n import tr
|
||||
|
||||
welcome.refresh(user_name="")
|
||||
|
||||
assert welcome.greeting_label.text() == tr("welcome.greeting_anon")
|
||||
|
||||
|
||||
# ---- i18n ----------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize("key", [
|
||||
"welcome.greeting", "welcome.greeting_anon", "welcome.meta_project",
|
||||
"welcome.meta_files", "welcome.meta_skills",
|
||||
"welcome.card_docs", "welcome.card_docs_sub", "welcome.prompt_docs",
|
||||
"welcome.card_data", "welcome.card_data_sub", "welcome.prompt_data",
|
||||
"welcome.card_schedule", "welcome.card_schedule_sub", "welcome.prompt_schedule",
|
||||
"welcome.card_graph", "welcome.card_graph_sub", "welcome.prompt_graph",
|
||||
])
|
||||
def test_key_co_du_ba_ngon_ngu(key):
|
||||
from cowork_local import i18n
|
||||
|
||||
for lang in ("en", "ja", "vi"):
|
||||
assert i18n.STRINGS[key].get(lang), f"{key} thiếu {lang}"
|
||||
|
||||
|
||||
def test_doi_ngon_ngu_thi_the_doi_theo(welcome):
|
||||
from cowork_local import i18n
|
||||
|
||||
truoc = i18n.get_language()
|
||||
try:
|
||||
i18n.set_language("vi")
|
||||
welcome.refresh(user_name="x", project="p")
|
||||
vi = welcome.cards[0].title_label.text()
|
||||
i18n.set_language("ja")
|
||||
ja = welcome.cards[0].title_label.text()
|
||||
finally:
|
||||
i18n.set_language(truoc)
|
||||
|
||||
assert vi != ja, "thẻ không được áp lại khi đổi ngôn ngữ"
|
||||
|
||||
|
||||
# ---- màu lấy từ theme ----------------------------------------------------
|
||||
|
||||
def test_duoc_style_bang_token_trong_theme():
|
||||
"""Guardrail G4: ngoài theme/ không file nào được đặt tên một màu."""
|
||||
from pathlib import Path
|
||||
from cowork_local.theme.qss import _TEMPLATE
|
||||
|
||||
for rule in ("QWidget#chatWelcome", "QPushButton#welcomeCard",
|
||||
"QLabel#welcomeGreeting"):
|
||||
assert rule in _TEMPLATE.template, f"{rule} chưa được style trong theme"
|
||||
src = (Path(__file__).resolve().parents[2]
|
||||
/ "presentation" / "chat" / "chat_welcome.py").read_text(encoding="utf-8")
|
||||
assert "setStyleSheet" not in src
|
||||
Reference in New Issue
Block a user