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:
@@ -9,6 +9,64 @@ from __future__ import annotations
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
# ---- man gioi thieu cua khung chat (trang thai rong) ------------------
|
||||
"welcome.greeting": {
|
||||
"en": "Hi {name} — where would you like to start?",
|
||||
"ja": "{name} さん、どこから始めましょうか?",
|
||||
"vi": "Chào {name} — bắt đầu từ đâu?"},
|
||||
"welcome.greeting_anon": {
|
||||
"en": "Where would you like to start?", "ja": "どこから始めましょうか?",
|
||||
"vi": "Bắt đầu từ đâu?"},
|
||||
"welcome.meta_project": {
|
||||
"en": "Working in {name}", "ja": "{name} で作業中",
|
||||
"vi": "Đang làm trong {name}"},
|
||||
"welcome.meta_files": {
|
||||
"en": "{n} file(s) in the local folder", "ja": "ローカルフォルダに {n} 件",
|
||||
"vi": "{n} tệp trong thư mục local"},
|
||||
"welcome.meta_skills": {
|
||||
"en": "Skills: {n} on", "ja": "スキル: {n} 個オン", "vi": "Skills: {n} bật"},
|
||||
|
||||
"welcome.card_docs": {
|
||||
"en": "Summarise documents", "ja": "ドキュメントを要約", "vi": "Tóm tắt tài liệu"},
|
||||
"welcome.card_docs_sub": {
|
||||
"en": "Read the files in the local folder", "ja": "ローカルフォルダのファイルを読む",
|
||||
"vi": "Đọc các tệp trong thư mục local"},
|
||||
"welcome.prompt_docs": {
|
||||
"en": "Read the files in this project's folder and summarise each one.",
|
||||
"ja": "このプロジェクトのフォルダにあるファイルを読み、それぞれ要約してください。",
|
||||
"vi": "Đọc các tệp trong thư mục của project này và tóm tắt từng tệp."},
|
||||
|
||||
"welcome.card_data": {
|
||||
"en": "Analyse data", "ja": "データを分析", "vi": "Phân tích dữ liệu"},
|
||||
"welcome.card_data_sub": {
|
||||
"en": "Spreadsheets, CSV, logs", "ja": "表計算、CSV、ログ",
|
||||
"vi": "Bảng tính, CSV, log"},
|
||||
"welcome.prompt_data": {
|
||||
"en": "Analyse the spreadsheet/CSV/log files in this folder and report what stands out.",
|
||||
"ja": "このフォルダの表計算/CSV/ログを分析し、目立つ点を報告してください。",
|
||||
"vi": "Phân tích các tệp bảng tính/CSV/log trong thư mục này và nêu những điểm đáng chú ý."},
|
||||
|
||||
"welcome.card_schedule": {
|
||||
"en": "Set up a schedule", "ja": "スケジュールを作成", "vi": "Dựng lịch chạy"},
|
||||
"welcome.card_schedule_sub": {
|
||||
"en": "Create a daily Schedule Task", "ja": "毎日実行する Schedule Task を作成",
|
||||
"vi": "Tạo Schedule Task hàng ngày"},
|
||||
"welcome.prompt_schedule": {
|
||||
"en": "Help me set up a Schedule Task that runs every day. Ask me what it should do.",
|
||||
"ja": "毎日実行する Schedule Task の作成を手伝ってください。何をするか質問してください。",
|
||||
"vi": "Giúp tôi dựng một Schedule Task chạy hàng ngày. Hỏi tôi nó cần làm gì."},
|
||||
|
||||
"welcome.card_graph": {
|
||||
"en": "Ask GraphRAG", "ja": "GraphRAG に質問", "vi": "Hỏi GraphRAG"},
|
||||
"welcome.card_graph_sub": {
|
||||
"en": "Query this project's knowledge graph",
|
||||
"ja": "このプロジェクトの知識グラフを検索",
|
||||
"vi": "Truy vấn đồ thị tri thức của project"},
|
||||
"welcome.prompt_graph": {
|
||||
"en": "Using this project's knowledge graph, explain how the main pieces fit together.",
|
||||
"ja": "このプロジェクトの知識グラフを使って、主要な要素の関係を説明してください。",
|
||||
"vi": "Dùng đồ thị tri thức của project này, giải thích các phần chính ghép với nhau thế nào."},
|
||||
|
||||
"chatpanel.agent_tooltip": {
|
||||
"en": "Model/agent for THIS tab — independent of the other tab",
|
||||
"ja": "このタブ専用のモデル/エージェント(他のタブとは独立)",
|
||||
|
||||
@@ -27,6 +27,7 @@ from ...state import AppContext
|
||||
from ...theme import current_palette
|
||||
from .chat_bubble_style import ThinkingIndicator
|
||||
from .chat_history_widget import ChatView
|
||||
from .chat_welcome import ChatWelcome
|
||||
from .composer_widget import Composer
|
||||
from ...ui.icons import collapse_right_icon, icon as app_icon
|
||||
from ...ui.osutil import is_image, open_path
|
||||
@@ -43,7 +44,13 @@ class ChatPanelLayoutMixin:
|
||||
cc = QVBoxLayout(chat_col)
|
||||
cc.setContentsMargins(0, 0, 0, 0)
|
||||
cc.setSpacing(0)
|
||||
# Man gioi thieu chiem dung cho cua khung chat va thay the no khi hoi
|
||||
# thoai con rong — hai thu khong bao gio cung hien.
|
||||
self.welcome = ChatWelcome()
|
||||
self.welcome.suggestion_picked.connect(self._use_suggestion)
|
||||
cc.addWidget(self.welcome, 1)
|
||||
cc.addWidget(self.chat_view, 1)
|
||||
self.chat_view.hide() # phien moi thi rong -> man gioi thieu di truoc
|
||||
self.thinking = ThinkingIndicator() # animated "working…" line while we wait
|
||||
cc.addWidget(self.thinking)
|
||||
self.center_split = QSplitter(Qt.Horizontal)
|
||||
@@ -148,3 +155,61 @@ class ChatPanelLayoutMixin:
|
||||
self.center_split.setChildrenCollapsible(False)
|
||||
self.center_split.setSizes([820, 220])
|
||||
on_language_changed(self._retranslate_base)
|
||||
|
||||
# ---- man gioi thieu ----------------------------------------------------
|
||||
def _use_suggestion(self, text: str) -> None:
|
||||
"""Thẻ gợi ý được bấm: ĐIỀN vào ô nhập, 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.
|
||||
"""
|
||||
self.composer.input.setPlainText(text)
|
||||
self.composer.input.setFocus()
|
||||
|
||||
def show_welcome(self, show: bool) -> None:
|
||||
"""Bật màn giới thiệu (hội thoại rỗng) hoặc khung chat (đã có tin)."""
|
||||
welcome = getattr(self, "welcome", None)
|
||||
if welcome is None:
|
||||
return
|
||||
welcome.setVisible(show)
|
||||
self.chat_view.setVisible(not show)
|
||||
if show:
|
||||
welcome.refresh(**self._welcome_context())
|
||||
|
||||
def _welcome_context(self) -> dict:
|
||||
"""Dữ liệu cho dòng bối cảnh. Không biết thì trả -1, KHÔNG trả 0.
|
||||
|
||||
Hiện "0 tệp" khi người dùng vừa nhìn thấy tệp trong thư mục còn tệ hơn
|
||||
là bỏ mảnh đó khỏi dòng meta.
|
||||
"""
|
||||
from pathlib import Path as _P
|
||||
|
||||
ten = ""
|
||||
try:
|
||||
from ...core.projects import load_project
|
||||
project = load_project(self.project_id) if getattr(self, "project_id", "") else None
|
||||
ten = project.name if project is not None else ""
|
||||
except Exception: # noqa: BLE001
|
||||
ten = ""
|
||||
|
||||
so_tep = -1
|
||||
try:
|
||||
folder = self.workspace_dir()
|
||||
if folder is not None and _P(folder).is_dir():
|
||||
so_tep = sum(1 for f in _P(folder).rglob("*")
|
||||
if f.is_file() and f.suffix.lower() in self._INPUT_EXTS)
|
||||
except Exception: # noqa: BLE001
|
||||
so_tep = -1
|
||||
|
||||
so_skill = -1
|
||||
try:
|
||||
from ...core.skills import list_skills
|
||||
so_skill = sum(1 for sk in list_skills() if getattr(sk, "enabled", False))
|
||||
except Exception: # noqa: BLE001
|
||||
so_skill = -1
|
||||
|
||||
# Ten nguoi dung do cua so chinh giu (app.py truyen xuong MainWindow).
|
||||
window = self.window()
|
||||
return {"user_name": getattr(window, "_user_name", "") or "",
|
||||
"project": ten, "files": so_tep, "skills": so_skill}
|
||||
|
||||
@@ -194,6 +194,8 @@ class ChatSessionMixin:
|
||||
"""
|
||||
from ...core.history import new_session_id
|
||||
|
||||
self.show_welcome(True) # hội thoại rỗng -> màn giới thiệu
|
||||
|
||||
# Allowed while work is running: current turns keep going in the background.
|
||||
self._detach_live_turns()
|
||||
self.messages = []
|
||||
@@ -230,6 +232,8 @@ class ChatSessionMixin:
|
||||
# turn must NOT tear down its live rendering — just no-op.
|
||||
if sid == self.session_id and self._view_busy():
|
||||
return
|
||||
# Hoi thoai da luu thi co tin nhan -> khung chat, khong phai man gioi thieu.
|
||||
self.show_welcome(not (conv.get("messages") or []))
|
||||
self._detach_live_turns()
|
||||
self.session_id = sid
|
||||
self.title = conv.get("title", "")
|
||||
|
||||
@@ -65,6 +65,10 @@ class ChatTurnRunnerMixin:
|
||||
attachments = attachments or []
|
||||
typed = text
|
||||
prefix, request, info = self._apply_skill_command(text)
|
||||
# Moi duong tra ve som duoi day cung them mot bong nguoi dung vao khung,
|
||||
# nen man gioi thieu phai nhuong cho ngay tai day — dat sau tung
|
||||
# add_user() thi de sot dung mot nhanh, va nhanh do se hien ca hai thu.
|
||||
self.show_welcome(False)
|
||||
if info is not None:
|
||||
# A local /skill command (list / select / error) — answer inline.
|
||||
self.chat_view.add_user(typed)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
"""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 — một trong bốn trạng thái mà mọi khung dữ liệu phải
|
||||
có (xem ``agent/checklist/ux_review.md``), và là trạng thái duy nhất người dùng
|
||||
nhìn thấy trước khi họ gõ chữ đầu tiên.
|
||||
|
||||
Bốn thẻ gợi ý ĐIỀN vào ô nhập chứ không gửi luôn: câu gợi ý là điểm bắt đầu, và
|
||||
người dùng gần như luôn cần thêm chi tiết của riêng họ trước khi gửi. Gửi ngay
|
||||
sẽ tiêu một lượt gọi model cho một câu hỏi chung chung.
|
||||
|
||||
Dấu trang trí phía trên lời chào không bấm được — nó là một dấu hiệu thị giác,
|
||||
không phải nút. Một nút không làm gì tệ hơn không có nút.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QGridLayout, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ...i18n import on_language_changed, tr
|
||||
from ...ui.icons import icon
|
||||
|
||||
#: (khoá tiêu đề, khoá mô tả, khoá câu gợi ý, tên icon) cho từng thẻ.
|
||||
_CARDS = (
|
||||
("welcome.card_docs", "welcome.card_docs_sub", "welcome.prompt_docs", "file"),
|
||||
("welcome.card_data", "welcome.card_data_sub", "welcome.prompt_data", "table"),
|
||||
("welcome.card_schedule", "welcome.card_schedule_sub", "welcome.prompt_schedule", "schedule"),
|
||||
("welcome.card_graph", "welcome.card_graph_sub", "welcome.prompt_graph", "graph"),
|
||||
)
|
||||
|
||||
|
||||
class _Card(QPushButton):
|
||||
"""Một thẻ gợi ý: icon, tiêu đề, và một dòng mô tả bên dưới."""
|
||||
|
||||
def __init__(self, title_key: str, sub_key: str, icon_name: str,
|
||||
parent: QWidget | None = None) -> None:
|
||||
"""Thẻ gợi ý. Là ``QPushButton`` để có sẵn hover, focus và bàn phím."""
|
||||
super().__init__(parent)
|
||||
self._title_key = title_key
|
||||
self._sub_key = sub_key
|
||||
self.setObjectName("welcomeCard")
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
row = QHBoxLayout(self)
|
||||
row.setContentsMargins(12, 10, 12, 10)
|
||||
row.setSpacing(10)
|
||||
|
||||
self._icon = QLabel()
|
||||
self._icon.setPixmap(icon(icon_name).pixmap(18, 18))
|
||||
self._icon.setAlignment(Qt.AlignTop)
|
||||
row.addWidget(self._icon)
|
||||
|
||||
col = QVBoxLayout()
|
||||
col.setContentsMargins(0, 0, 0, 0)
|
||||
col.setSpacing(2)
|
||||
self.title_label = QLabel()
|
||||
self.title_label.setObjectName("welcomeCardTitle")
|
||||
self.sub_label = QLabel()
|
||||
self.sub_label.setObjectName("hint")
|
||||
self.sub_label.setWordWrap(True)
|
||||
col.addWidget(self.title_label)
|
||||
col.addWidget(self.sub_label)
|
||||
row.addLayout(col, 1)
|
||||
|
||||
self.retranslate()
|
||||
|
||||
def retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
self.title_label.setText(tr(self._title_key))
|
||||
self.sub_label.setText(tr(self._sub_key))
|
||||
# Nhãn của chính QPushButton để rỗng — chữ do hai QLabel bên trong vẽ,
|
||||
# đặt cả hai chỗ sẽ in đè lên nhau.
|
||||
self.setAccessibleName(tr(self._title_key))
|
||||
|
||||
|
||||
class ChatWelcome(QWidget):
|
||||
"""Trạng thái rỗng của khung chat: lời chào, dòng bối cảnh, bốn thẻ gợi ý."""
|
||||
|
||||
#: Người dùng chọn một thẻ. Mang theo câu gợi ý để chỗ nhận điền vào ô nhập.
|
||||
suggestion_picked = Signal(str)
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
"""Dựng màn giới thiệu. Chữ được điền qua :meth:`refresh`."""
|
||||
super().__init__(parent)
|
||||
self.setObjectName("chatWelcome")
|
||||
self._user_name = ""
|
||||
self._meta_parts: list = []
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(24, 24, 24, 24)
|
||||
root.addStretch(1)
|
||||
|
||||
mark_row = QHBoxLayout()
|
||||
mark_row.addStretch(1)
|
||||
self._mark = QLabel()
|
||||
self._mark.setObjectName("welcomeMark")
|
||||
self._mark.setPixmap(icon("sparkle").pixmap(20, 20))
|
||||
self._mark.setAlignment(Qt.AlignCenter)
|
||||
self._mark.setFixedSize(38, 38)
|
||||
mark_row.addWidget(self._mark)
|
||||
mark_row.addStretch(1)
|
||||
root.addLayout(mark_row)
|
||||
root.addSpacing(14)
|
||||
|
||||
self.greeting_label = QLabel()
|
||||
self.greeting_label.setObjectName("welcomeGreeting")
|
||||
self.greeting_label.setAlignment(Qt.AlignCenter)
|
||||
root.addWidget(self.greeting_label)
|
||||
|
||||
self.meta_label = QLabel()
|
||||
self.meta_label.setObjectName("hint")
|
||||
self.meta_label.setAlignment(Qt.AlignCenter)
|
||||
root.addWidget(self.meta_label)
|
||||
root.addSpacing(18)
|
||||
|
||||
grid_row = QHBoxLayout()
|
||||
grid_row.addStretch(1)
|
||||
grid_host = QWidget()
|
||||
grid_host.setMaximumWidth(460)
|
||||
grid = QGridLayout(grid_host)
|
||||
grid.setContentsMargins(0, 0, 0, 0)
|
||||
grid.setSpacing(10)
|
||||
self.cards: list = []
|
||||
for i, (title_key, sub_key, prompt_key, icon_name) in enumerate(_CARDS):
|
||||
card = _Card(title_key, sub_key, icon_name)
|
||||
card.clicked.connect(
|
||||
lambda _checked=False, key=prompt_key: self.suggestion_picked.emit(tr(key)))
|
||||
grid.addWidget(card, i // 2, i % 2)
|
||||
self.cards.append(card)
|
||||
grid_row.addWidget(grid_host)
|
||||
grid_row.addStretch(1)
|
||||
root.addLayout(grid_row)
|
||||
|
||||
root.addStretch(2)
|
||||
|
||||
on_language_changed(self._retranslate)
|
||||
|
||||
# ---- nội dung ----------------------------------------------------------
|
||||
|
||||
def refresh(self, user_name: str = "", project: str = "",
|
||||
files: int = -1, skills: int = -1) -> None:
|
||||
"""Cập nhật lời chào và dòng bối cảnh.
|
||||
|
||||
``files`` / ``skills`` bằng ``-1`` nghĩa là KHÔNG BIẾT, và phần đó bị bỏ
|
||||
khỏi dòng meta — thà thiếu một mảnh còn hơn hiện số 0 mà người dùng vừa
|
||||
thấy có tệp trong thư mục.
|
||||
"""
|
||||
self._user_name = (user_name or "").strip()
|
||||
parts = []
|
||||
if (project or "").strip():
|
||||
parts.append(tr("welcome.meta_project", name=project.strip()))
|
||||
if files >= 0:
|
||||
parts.append(tr("welcome.meta_files", n=files))
|
||||
if skills >= 0:
|
||||
parts.append(tr("welcome.meta_skills", n=skills))
|
||||
self._meta_parts = parts
|
||||
self._retranslate()
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn."""
|
||||
if self._user_name:
|
||||
self.greeting_label.setText(tr("welcome.greeting", name=self._user_name))
|
||||
else:
|
||||
self.greeting_label.setText(tr("welcome.greeting_anon"))
|
||||
self.meta_label.setText(" · ".join(self._meta_parts))
|
||||
self.meta_label.setVisible(bool(self._meta_parts))
|
||||
for card in self.cards:
|
||||
card.retranslate()
|
||||
@@ -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
|
||||
@@ -303,6 +303,21 @@ QPushButton#detailCopyBtn:hover { background: rgba(255,255,255,0.25); }
|
||||
QLabel { background: transparent; }
|
||||
QLabel#hint { color: $text_muted; }
|
||||
|
||||
/* Man gioi thieu cua khung chat (trang thai rong). */
|
||||
QWidget#chatWelcome { background: $bg; }
|
||||
QLabel#welcomeGreeting { color: $text; font-size: 19px; font-weight: 600; }
|
||||
QLabel#welcomeMark {
|
||||
background: $accent_soft; border: 1px solid $border_strong;
|
||||
border-radius: ${radius}px;
|
||||
}
|
||||
QPushButton#welcomeCard {
|
||||
background: $surface; border: 1px solid $border; border-radius: ${radius}px;
|
||||
text-align: left; padding: 0px;
|
||||
}
|
||||
QPushButton#welcomeCard:hover { background: $hover; border-color: $border_strong; }
|
||||
QPushButton#welcomeCard:focus { border: 1px solid $focus_ring; }
|
||||
QLabel#welcomeCardTitle { color: $text; font-weight: 600; }
|
||||
|
||||
/* Panel "dang tai" phu len khung do thi GraphRAG. Dung khung QWebEngineView la
|
||||
viec DONG BO tren GUI thread (~1-2s), nen thanh tien trinh o day khong chay
|
||||
duoc trong giai doan do — panel van phai nhin thay duoc, va do la ly do no co
|
||||
|
||||
Reference in New Issue
Block a user