fix các bug theo yêu cầu https://fptsoftware362-my.sharepoint.com/❌/g/personal/nampdt_fpt_com/IQAHBJ4A9xqDTLgvt2bhukJEAdRB5LRz2hbJpTivvIiBSYM?wdExp=TEAMS-TREATMENT&web=1&isSPOFile=1&ovuser=f01e930a-b52e-42b1-b70f-a8882b5d043b%2CAnhTNM1%40fpt.com&clickparams=eyJBcHBOYW1lIjoiVGVhbXMtRGVza3RvcCIsIkFwcFZlcnNpb24iOiI0OS8yNjA4MTMxOTMxNyIsIkhhc0ZlZGVyYXRlZFVzZXIiOmZhbHNlfQ%3D%3D --------- Co-authored-by: Duy Le Huu <duylh19@fpt.com> Reviewed-on: #10 Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
This commit was merged in pull request #10.
This commit is contained in:
@@ -59,9 +59,19 @@ class AttachmentMixin:
|
||||
lines = [text] if text else []
|
||||
|
||||
# --- User-attached files ---
|
||||
# Đường dẫn đã giải quyết của các tệp đính kèm, để vòng quét thư mục
|
||||
# phía sau không gửi lại chính chúng một lần nữa.
|
||||
da_dinh_kem = set()
|
||||
if has_attachments:
|
||||
lines.append("\n[Attachments] — read and use these files to answer the request:")
|
||||
lines.append(
|
||||
"\n[Attachments] — the user attached these files for THIS request. "
|
||||
"They are the PRIMARY subject: read them in full and base the answer "
|
||||
"on them. Anything listed further below is background context only.")
|
||||
for p in attachments:
|
||||
try:
|
||||
da_dinh_kem.add(str(Path(p).resolve()))
|
||||
except OSError:
|
||||
pass
|
||||
lines.extend(self._read_one_attachment(p, limit, notify))
|
||||
|
||||
# --- Auto-load existing workspace/output folder files as input data ---
|
||||
@@ -74,10 +84,10 @@ class AttachmentMixin:
|
||||
if workspace is not None:
|
||||
lines.extend(self._folder_input_lines(
|
||||
workspace,
|
||||
"[Workspace files] — existing files in output folder, "
|
||||
"read and use as input data. The user expects you to "
|
||||
"process these files automatically:",
|
||||
limit, max_files, notify))
|
||||
"[Workspace files] — other files that happen to sit in the output "
|
||||
"folder. Background context; do NOT let them displace the "
|
||||
"attached files or the user's own question:",
|
||||
limit, max_files, notify, da_dinh_kem))
|
||||
|
||||
# --- Project knowledge (Claude-Projects style) ---
|
||||
# Only scanned separately when it's a DIFFERENT folder from the
|
||||
@@ -88,31 +98,44 @@ class AttachmentMixin:
|
||||
if knowledge is not None and knowledge != workspace:
|
||||
lines.extend(self._folder_input_lines(
|
||||
knowledge,
|
||||
"[Project files] — shared knowledge files of this project, "
|
||||
"available to every conversation in it. Read and use them "
|
||||
"as context for the request:",
|
||||
limit, max_files, notify))
|
||||
"[Project files] — shared knowledge of this project. Background "
|
||||
"context; do NOT let them displace the attached files or "
|
||||
"the user's own question:",
|
||||
limit, max_files, notify, da_dinh_kem))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _folder_input_lines(self, folder: Path, header: str, limit: int,
|
||||
max_files: int, notify=None) -> list:
|
||||
max_files: int, notify=None, skip=frozenset()) -> list:
|
||||
"""Embed a folder's readable files into the prompt — recursing into
|
||||
every sub-folder, any depth, not just the top level, so files placed
|
||||
in nested folders are read and processed too (same per-message file
|
||||
cap as manual attachments — Settings → Attachments → max files;
|
||||
0 = unlimited — so a folder with dozens of files can't blow the
|
||||
context window)."""
|
||||
from pathlib import Path as _P
|
||||
|
||||
from ...core.doc_extract import find_input_files
|
||||
|
||||
out: list = []
|
||||
shown, total = find_input_files(folder, self._INPUT_EXTS, max_files)
|
||||
# Bo qua tep nguoi dung DA dinh kem tuong minh. Tep dinh kem thuong nam
|
||||
# ngay trong thu muc workspace, nen khong loc thi cung mot tai lieu di vao
|
||||
# prompt HAI lan: mot lan duoi [Attachments], mot lan duoi [Workspace
|
||||
# files]. Voi tai lieu dai, ban thu hai vua nhan doi ngu canh vua khien
|
||||
# model khong biet ban nao la ban duoc hoi.
|
||||
# Số tệp thư mục này thực sự trả về, ĐO TRƯỚC khi lọc trùng: dòng cảnh
|
||||
# báo bên dưới nói về giới hạn mỗi lượt, nên đếm cả tệp bị lọc vì đã
|
||||
# đính kèm sẽ báo sai là "không nạp được".
|
||||
so_lay_duoc = len(shown)
|
||||
if skip:
|
||||
shown = [f for f in shown if str(_P(f).resolve()) not in skip]
|
||||
if shown:
|
||||
out.append("\n" + header)
|
||||
for f in shown:
|
||||
out.extend(self._read_one_attachment(str(f), limit, notify))
|
||||
if total > len(shown):
|
||||
skipped = total - len(shown)
|
||||
if total > so_lay_duoc:
|
||||
skipped = total - so_lay_duoc
|
||||
out.append(f"…({skipped} more files in the folder were not "
|
||||
"loaded — per-message attachment limit; mention a "
|
||||
"file by name if the user asks about it)")
|
||||
|
||||
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from cowork_local.i18n import tr
|
||||
from cowork_local.i18n import bind_dynamic, bind_tip, tr
|
||||
from cowork_local.theme import current_palette
|
||||
from cowork_local.ui.icons import icon
|
||||
|
||||
@@ -57,7 +57,7 @@ class AudioRecorderWidget(QWidget):
|
||||
# Record / Stop toggle button
|
||||
self.record_btn = QPushButton()
|
||||
self.record_btn.setIcon(icon("microphone"))
|
||||
self.record_btn.setToolTip(tr("chat.record_audio_start") if tr("chat.record_audio_start") != "chat.record_audio_start" else "Record Voice Note")
|
||||
bind_dynamic(self.record_btn, self._sync_record_tip)
|
||||
self.record_btn.setFixedSize(32, 32)
|
||||
self.record_btn.clicked.connect(self.toggle_recording)
|
||||
layout.addWidget(self.record_btn)
|
||||
@@ -78,7 +78,7 @@ class AudioRecorderWidget(QWidget):
|
||||
|
||||
self.cancel_btn = QPushButton()
|
||||
self.cancel_btn.setIcon(icon("x"))
|
||||
self.cancel_btn.setToolTip("Cancel recording")
|
||||
bind_tip(self.cancel_btn, "chat.record_audio_cancel")
|
||||
self.cancel_btn.setFixedSize(24, 24)
|
||||
self.cancel_btn.clicked.connect(self.cancel_recording)
|
||||
status_layout.addWidget(self.cancel_btn)
|
||||
@@ -106,7 +106,7 @@ class AudioRecorderWidget(QWidget):
|
||||
self.timer_label.setText("00:00")
|
||||
self.status_container.setVisible(True)
|
||||
self.record_btn.setIcon(icon("square"))
|
||||
self.record_btn.setToolTip("Stop Recording")
|
||||
self._sync_record_tip()
|
||||
self.record_btn.setStyleSheet("background-color: #fca5a5; color: #991b1b;")
|
||||
self._timer.start()
|
||||
self.recording_started.emit()
|
||||
@@ -137,7 +137,12 @@ class AudioRecorderWidget(QWidget):
|
||||
self.status_container.setVisible(False)
|
||||
self.record_btn.setIcon(icon("microphone"))
|
||||
self.record_btn.setStyleSheet("")
|
||||
self.record_btn.setToolTip("Record Voice Note")
|
||||
self._sync_record_tip()
|
||||
|
||||
def _sync_record_tip(self) -> None:
|
||||
"""Tooltip nút ghi âm nói việc nó sẽ làm tiếp, theo trạng thái hiện tại."""
|
||||
self.record_btn.setToolTip(tr("chat.record_audio_stop" if self._is_recording
|
||||
else "chat.record_audio_start"))
|
||||
|
||||
def _on_tick(self) -> None:
|
||||
"""Update recording duration display every second."""
|
||||
|
||||
@@ -141,7 +141,7 @@ class ChatAgentsMixin:
|
||||
if not items and self.agent_combo.count() == 0:
|
||||
# No models found and none configured — placeholder with data=None so
|
||||
# we fall back to the provider's default model (never a fake name).
|
||||
self.agent_combo.addItem("(provider default)", None)
|
||||
self.agent_combo.addItem(tr("chat.provider_default_item"), None)
|
||||
keep_data = (f"{self._ADMIN_AGENT_PREFIX}{self._admin_agent.agent_id}"
|
||||
if getattr(self, "_admin_agent", None) is not None else keep)
|
||||
idx = self.agent_combo.findData(keep_data) if keep_data else -1
|
||||
|
||||
@@ -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,54 @@ 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
|
||||
|
||||
# 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}
|
||||
|
||||
@@ -11,9 +11,9 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from ...core.worker import AgentWorker
|
||||
from ...i18n import tr
|
||||
from ...ui.dialog_buttons import confirm
|
||||
|
||||
|
||||
class ChatSessionMixin:
|
||||
@@ -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", "")
|
||||
@@ -304,7 +308,7 @@ class ChatSessionMixin:
|
||||
prompt = tr("chatpanel.delete_confirm_files", n=len(files), preview=preview)
|
||||
else:
|
||||
prompt = tr("chatpanel.delete_confirm_plain")
|
||||
if QMessageBox.question(self, tr("chatpanel.delete_confirm_title"), prompt) != QMessageBox.Yes:
|
||||
if not confirm(self, tr("chatpanel.delete_confirm_title"), prompt):
|
||||
return
|
||||
for bubble in turn.get("bubbles", []):
|
||||
bubble.setParent(None)
|
||||
|
||||
@@ -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,215 @@
|
||||
"""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, QSizePolicy, QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ...i18n import on_language_changed, tr
|
||||
from ...ui.icons import icon
|
||||
|
||||
#: Width of the four-card block. A FLOOR for the cap, not a fixed number: the
|
||||
#: block never gets narrower than this, but the cap grows when the text needs
|
||||
#: more room. One number measured against English at 100% scale is exactly how
|
||||
#: the titles end up clipped in Vietnamese and Japanese (``qt_pitfalls.md`` P02).
|
||||
_GRID_WIDTH_FLOOR = 460
|
||||
|
||||
#: (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)
|
||||
# Vertically it must be able to GROW: QPushButton defaults to Fixed, so
|
||||
# a card whose description fits on one line was centred inside a row as
|
||||
# tall as its two-line neighbour — two cards side by side, staggered and
|
||||
# of different heights.
|
||||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding)
|
||||
|
||||
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()
|
||||
|
||||
# ---- size: taken from the child layout, not from the button's own text -- #
|
||||
# QPushButton computes sizeHint/minimumSizeHint from ITS OWN text and icon
|
||||
# and ignores the child layout. This card leaves both of those empty on
|
||||
# purpose (the two QLabels below draw the text; a non-empty text() prints
|
||||
# on top of them), so the button reported 54x15 while its layout asked for
|
||||
# 258x48 — the two QLabels and the icon cell were handed 0px of height, and
|
||||
# what the user saw was four empty frames with no text and no icon. The two
|
||||
# overrides below report the size the content actually needs.
|
||||
|
||||
def sizeHint(self): # noqa: N802 - Qt override
|
||||
"""Size the card's own content needs, not the (empty) button label."""
|
||||
return self.layout().sizeHint()
|
||||
|
||||
def minimumSizeHint(self): # noqa: N802 - Qt override
|
||||
"""Floor comes from the child layout, for the same reason."""
|
||||
return self.layout().minimumSize()
|
||||
|
||||
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))
|
||||
# New text means a new content size — Japanese and Vietnamese are not
|
||||
# the same length, and sizeHint is computed from those two QLabels.
|
||||
self.updateGeometry()
|
||||
|
||||
|
||||
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)
|
||||
self._grid_host = QWidget()
|
||||
self._grid = QGridLayout(self._grid_host)
|
||||
self._grid.setContentsMargins(0, 0, 0, 0)
|
||||
self._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)))
|
||||
self._grid.addWidget(card, i // 2, i % 2)
|
||||
self.cards.append(card)
|
||||
self._apply_grid_width()
|
||||
grid_row.addWidget(self._grid_host)
|
||||
grid_row.addStretch(1)
|
||||
root.addLayout(grid_row)
|
||||
|
||||
root.addStretch(2)
|
||||
|
||||
on_language_changed(self._retranslate)
|
||||
|
||||
def _apply_grid_width(self) -> None:
|
||||
"""Cap the card block at the wider of the design width and what text needs.
|
||||
|
||||
Recomputed on every language change: ``vi`` and ``ja`` labels are not
|
||||
the same length as ``en``, and a cap fixed at build time clips whichever
|
||||
language happens to be longer.
|
||||
"""
|
||||
self._grid_host.setMaximumWidth(
|
||||
max(_GRID_WIDTH_FLOOR, self._grid.sizeHint().width()))
|
||||
|
||||
# ---- nội dung ----------------------------------------------------------
|
||||
|
||||
def refresh(self, user_name: str = "", project: str = "",
|
||||
files: int = -1) -> None:
|
||||
"""Cập nhật lời chào và dòng bối cảnh.
|
||||
|
||||
``files`` 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.
|
||||
|
||||
Không hiện số skill đang bật: nó không giúp người dùng quyết định gõ gì
|
||||
vào ô nhập, mà lại chiếm một phần ba của dòng bối cảnh.
|
||||
"""
|
||||
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))
|
||||
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()
|
||||
self._apply_grid_width()
|
||||
Reference in New Issue
Block a user