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:
+6
-6
@@ -18,7 +18,7 @@ from typing import Dict, List, Optional
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout,
|
||||
QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, QMessageBox,
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMessageBox,
|
||||
QPushButton, QSplitter, QTableWidget, QTableWidgetItem, QTreeWidget,
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
@@ -28,6 +28,7 @@ from ..core import usage_tracker as ut
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from .dialog_buttons import ask_text, confirm, dialog_buttons
|
||||
from .icons import DOT_AMBER, icon
|
||||
from .widgets import fmt_tokens
|
||||
|
||||
@@ -151,7 +152,7 @@ class AccountEditDialog(QDialog):
|
||||
self.group_combo.setCurrentIndex(idx)
|
||||
self.group_combo.setEnabled(allow_group_edit)
|
||||
form.addRow(tr("accounts.f_group"), self.group_combo)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
form.addRow(buttons)
|
||||
@@ -529,9 +530,8 @@ class AccountsTab(QWidget):
|
||||
return
|
||||
if not self._is_admin() and account.role != "user":
|
||||
return # Sub-admin can only remove ordinary members, never another sub-admin
|
||||
if QMessageBox.question(
|
||||
self, tr("accounts.delete_title"),
|
||||
tr("accounts.delete_confirm", username=account.username)) != QMessageBox.Yes:
|
||||
if not confirm(self, tr("accounts.delete_title"),
|
||||
tr("accounts.delete_confirm", username=account.username)):
|
||||
return
|
||||
accounts.delete_account(account.username, self._accounts_dir())
|
||||
if account.group_id:
|
||||
@@ -703,7 +703,7 @@ class AccountsTab(QWidget):
|
||||
"""Hỏi tên rồi tạo một nhóm mới. Chỉ Admin."""
|
||||
if not self._is_admin():
|
||||
return
|
||||
name, ok = QInputDialog.getText(self, tr("accounts.new_group_title"), tr("accounts.f_group_name"))
|
||||
name, ok = ask_text(self, tr("accounts.new_group_title"), tr("accounts.f_group_name"))
|
||||
if not (ok and name.strip()):
|
||||
return
|
||||
group = groups.new_group(name.strip())
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Optional
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea,
|
||||
QListWidgetItem, QPlainTextEdit, QPushButton, QScrollArea,
|
||||
QSplitter, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ from ..core.custom_agents import (
|
||||
)
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import confirm
|
||||
from .icons import icon
|
||||
|
||||
|
||||
@@ -300,10 +301,8 @@ class AgentManagerTab(QWidget):
|
||||
agent = self._current_agent()
|
||||
if agent is None:
|
||||
return
|
||||
if QMessageBox.question(
|
||||
self, tr("agentmgr.delete_btn"),
|
||||
tr("agentmgr.delete_confirm", name=agent.name),
|
||||
) != QMessageBox.Yes:
|
||||
if not confirm(self, tr("agentmgr.delete_btn"),
|
||||
tr("agentmgr.delete_confirm", name=agent.name)):
|
||||
return
|
||||
delete_agent(agent.name)
|
||||
self._reload_list()
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import List
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QInputDialog,
|
||||
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
|
||||
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton,
|
||||
QVBoxLayout, QWidget,
|
||||
)
|
||||
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
|
||||
from ..core.co4e import PERMISSION_PRESETS, CustomAgent
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import ask_multiline, dialog_buttons
|
||||
from .icons import icon, icon_picker_combo
|
||||
|
||||
|
||||
@@ -114,7 +115,7 @@ class Co4EAgentDialog(QDialog):
|
||||
form.addRow(tr("co4e.f_attachments"), self.attach_list)
|
||||
form.addRow("", abtn)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
form.addRow(buttons)
|
||||
@@ -129,7 +130,7 @@ class Co4EAgentDialog(QDialog):
|
||||
role = self.role_edit.text().strip()
|
||||
if not name and not role:
|
||||
return
|
||||
hint, ok = QInputDialog.getMultiLineText(
|
||||
hint, ok = ask_multiline(
|
||||
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
|
||||
if not ok:
|
||||
return
|
||||
|
||||
+27
-13
@@ -390,21 +390,35 @@ class Co4ETab(
|
||||
|
||||
# ---- i18n -------------------------------------------------------------
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề các mục và tooltip."""
|
||||
for key in self._sections:
|
||||
self._sync_section_arrow(key)
|
||||
self.runs_more_btn.setToolTip(tr("co4e.tt_runs_tab"))
|
||||
self.wf_new_btn.setText(tr("co4e.new"))
|
||||
self.wf_new_btn.setToolTip(tr("co4e.tt_new_wf"))
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề các mục và tooltip.
|
||||
|
||||
Cột trái và trang Flow Status không có mặt ở đây: mỗi widget bên đó tự
|
||||
ràng buộc khoá dịch của mình tại chỗ dựng (``i18n.bind_*``), nên panel
|
||||
dùng ở đâu cũng đúng ngôn ngữ mà không cần ai nhớ hộ.
|
||||
"""
|
||||
self.runs_btn.setText(tr("co4e.runs_tab"))
|
||||
self.runs_btn.setToolTip(tr("co4e.tt_runs_tab"))
|
||||
self.runs_back_btn.setText(tr("co4e.back_to_flow"))
|
||||
self.runs_back_btn.setToolTip(tr("co4e.tt_back_to_flow"))
|
||||
self.runs_title.setText(tr("co4e.running_flows"))
|
||||
self.run_stop_btn.setText(tr("co4e.stop"))
|
||||
self.run_rename_btn.setText(tr("co4e.rename_run"))
|
||||
self.run_del_btn.setText(tr("co4e.delete_run"))
|
||||
self.run_clear_btn.setText(tr("co4e.clear_done"))
|
||||
# Flow toolbar.
|
||||
self.name_edit.setToolTip(tr("co4e.tt_flow_name"))
|
||||
self.add_step_btn.setText(tr("co4e.add"))
|
||||
self.add_step_btn.setToolTip(tr("co4e.tt_add_step"))
|
||||
self.save_btn.setText(tr("co4e.save"))
|
||||
self.save_btn.setToolTip(tr("co4e.tt_save"))
|
||||
self.save_tpl_btn.setToolTip(tr("co4e.tt_save_template"))
|
||||
self.mode_combo.setToolTip(tr("co4e.tt_mode"))
|
||||
# By position, from the same source the items were built from: the mode
|
||||
# string in each item's data is persisted, so it must survive a
|
||||
# translation untouched.
|
||||
for i, mode in enumerate(co4e.RUN_MODES):
|
||||
self.mode_combo.setItemText(i, tr(f"co4e.mode.{mode}"))
|
||||
self.run_btn.setToolTip(tr("co4e.tt_run"))
|
||||
# Not a plain tr(): this button reads "Dừng" while THIS flow is running.
|
||||
self._update_run_btn()
|
||||
# Step-config panel header. The toggle's tooltip names the action it
|
||||
# would perform, which depends on which way the panel is folded.
|
||||
self.config_title.setText(tr("co4e.config_title"))
|
||||
self.config_toggle_btn.setToolTip(tr(
|
||||
"co4e.tt_expand_config" if self._config_collapsed else "co4e.tt_collapse_config"))
|
||||
self._refresh_ws_folder_btn()
|
||||
self.runs_table.setHorizontalHeaderLabels([
|
||||
tr("co4e.runs_col_flow"), tr("co4e.runs_col_status"), tr("co4e.runs_col_steps"),
|
||||
|
||||
+13
-11
@@ -13,14 +13,15 @@ from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
|
||||
QDialog, QFormLayout, QFrame, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QScrollArea, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..i18n import bind_text, on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from .dialog_buttons import confirm
|
||||
from .ext_connector_dialog import ExtConnectorEditDialog
|
||||
from .icons import icon
|
||||
from .widgets import FlowLayout, ToggleSwitch, enable_height_for_width, style_card
|
||||
@@ -129,11 +130,13 @@ class ConnectorsPanel(QWidget):
|
||||
|
||||
Có một công tắc tổng: tắt là agent không nối ra connector ngoài nào cả.
|
||||
"""
|
||||
# Khoá dịch, không phải chữ sẵn: nhãn nhóm dựng lại trong _reload_connectors
|
||||
# nên tr() ở đó là đủ (xem i18n/connectors.py).
|
||||
_EXT_CATEGORY_LABELS = {
|
||||
"cad": "CAD (NX / CATIA / SolidWorks / AutoCAD)",
|
||||
"cae": "CAE (ANSA / ABAQUS / HyperWorks / ANSYS)",
|
||||
"ms365": "MS365 (Microsoft 365 / OneDrive / SharePoint)",
|
||||
"other": "Other (any generic MCP server)",
|
||||
"cad": "connectors.cat_cad",
|
||||
"cae": "connectors.cat_cae",
|
||||
"ms365": "connectors.cat_ms365",
|
||||
"other": "connectors.cat_other",
|
||||
}
|
||||
_EXT_CATEGORY_ICONS = {"cad": "wrench", "cae": "ruler", "ms365": "cloud", "other": "plug"}
|
||||
# TEMPORARY: only OneDrive + SharePoint (auto-connect via locally-synced
|
||||
@@ -160,7 +163,7 @@ class ConnectorsPanel(QWidget):
|
||||
self.connect_external_sw.toggled.connect(self._on_connect_external_toggled)
|
||||
lay.addWidget(self.connect_external_sw)
|
||||
|
||||
hint = QLabel(tr("settings.ext_hint"))
|
||||
hint = bind_text(QLabel(), "settings.ext_hint")
|
||||
hint.setObjectName("hint")
|
||||
hint.setWordWrap(True)
|
||||
lay.addWidget(hint)
|
||||
@@ -232,7 +235,7 @@ class ConnectorsPanel(QWidget):
|
||||
icon_lbl = QLabel()
|
||||
icon_lbl.setPixmap(icon(self._EXT_CATEGORY_ICONS.get(cat, "plug"), size=18).pixmap(18, 18))
|
||||
hdr.addWidget(icon_lbl)
|
||||
name, _, subtitle = self._EXT_CATEGORY_LABELS.get(cat, cat).partition(" (")
|
||||
name, _, subtitle = tr(self._EXT_CATEGORY_LABELS.get(cat, cat)).partition(" (")
|
||||
name_lbl = QLabel(name)
|
||||
name_lbl.setStyleSheet("font-weight:700;")
|
||||
hdr.addWidget(name_lbl)
|
||||
@@ -373,9 +376,8 @@ class ConnectorsPanel(QWidget):
|
||||
|
||||
def _delete_ext_entry(self, cat: str, entry: dict) -> None:
|
||||
"""Xoá một connector MCP sau khi hỏi xác nhận."""
|
||||
if QMessageBox.question(
|
||||
self, tr("settings.ext_delete_btn"),
|
||||
tr("settings.ext_delete_confirm", name=entry.get("name", ""))) != QMessageBox.Yes:
|
||||
if not confirm(self, tr("settings.ext_delete_btn"),
|
||||
tr("settings.ext_delete_confirm", name=entry.get("name", ""))):
|
||||
return
|
||||
self.ctx.config.ext_connectors[cat].remove(entry)
|
||||
self.ctx.save()
|
||||
|
||||
+10
-9
@@ -7,7 +7,6 @@ from PySide6.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..config import PROVIDER_LABELS
|
||||
from ..core import agent_roles
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
@@ -32,9 +31,6 @@ class CoworkTab(ChatPanel):
|
||||
|
||||
self._title_lbl = QLabel()
|
||||
self._title_lbl.setStyleSheet("font-weight:700; font-size:15px;")
|
||||
self.model_lbl = QLabel("")
|
||||
self.model_lbl.setObjectName("hint")
|
||||
|
||||
self.skills_btn = QPushButton()
|
||||
self.skills_btn.setIcon(icon("book"))
|
||||
self.skills_btn.clicked.connect(self._open_skills_manager)
|
||||
@@ -44,7 +40,6 @@ class CoworkTab(ChatPanel):
|
||||
self._new_btn.clicked.connect(self.new_session)
|
||||
|
||||
self.toolbar_layout.addWidget(self._title_lbl)
|
||||
self.toolbar_layout.addWidget(self.model_lbl)
|
||||
self.toolbar_layout.addStretch(1)
|
||||
self.toolbar_layout.addWidget(self.skills_btn)
|
||||
self.toolbar_layout.addWidget(self._new_btn)
|
||||
@@ -104,6 +99,10 @@ class CoworkTab(ChatPanel):
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
"""Áp lại chữ theo ngôn ngữ đang chọn cho tiêu đề và các nút trên thanh công cụ."""
|
||||
# ChatPanel dựng phần chrome dùng chung (nhãn Agent, nút Nén, tiêu đề ba
|
||||
# bảng tệp) nhưng KHÔNG tự đăng ký dịch lại — lớp con là chỗ duy nhất có
|
||||
# đăng ký, nên bỏ dòng này là toàn bộ phần đó đứng ở ngôn ngữ lúc dựng.
|
||||
self._retranslate_base()
|
||||
self.refresh_title()
|
||||
self.skills_btn.setText(tr("cowork.skills_btn"))
|
||||
self.skills_btn.setToolTip(tr("cowork.skills_tooltip"))
|
||||
@@ -377,10 +376,12 @@ class CoworkTab(ChatPanel):
|
||||
self._apply_output_folder_label()
|
||||
|
||||
def refresh_header(self) -> None:
|
||||
"""Cập nhật dòng "provider · model" trên thanh công cụ Cowork."""
|
||||
cfg = self.ctx.config
|
||||
label = PROVIDER_LABELS.get(cfg.active_provider, cfg.active_provider)
|
||||
self.model_lbl.setText(f"{label} · {cfg.model_label()}")
|
||||
"""Cập nhật các nhãn trên thanh công cụ Cowork.
|
||||
|
||||
Dòng "provider · model" từng nằm ngay sau chữ "Cowork" đã được gỡ: nó
|
||||
lặp lại thông tin mà bộ chọn provider ở thanh trên đang hiển thị, và
|
||||
chiếm chỗ đắt nhất trên thanh công cụ cho một thứ chỉ để đọc.
|
||||
"""
|
||||
self._apply_output_folder_label() # picks up edits made via Settings too
|
||||
|
||||
def build_job(self, text: str, messages, out_dir):
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Nút hộp thoại mang chữ của ứng dụng, không phải chữ mặc định của Qt.
|
||||
|
||||
``QDialogButtonBox(Save | Cancel)``, ``QMessageBox.question`` và
|
||||
``QInputDialog.get*`` lấy nhãn nút từ bảng dịch của CHÍNH Qt. Ứng dụng không cài
|
||||
``QTranslator`` nào — và bản PySide6 đang dùng cũng không đóng gói sẵn file
|
||||
``qtbase_*.qm`` nào để cài — nên bảng đó luôn rơi về tiếng Anh: chọn tiếng Nhật
|
||||
thì hộp thoại vẫn hiện "Save" / "Cancel" / "&Yes" / "&No". Đây là lỗi người dùng
|
||||
báo, và nó không nằm ở dây nối i18n của dự án — ``tr()`` chưa từng được hỏi tới.
|
||||
|
||||
Các hàm dưới đây gán nhãn của dự án đè lên nhãn Qt. Dùng chúng thay cho việc
|
||||
dựng ``QDialogButtonBox`` / gọi ``QMessageBox.question`` / ``QInputDialog.get*``
|
||||
trực tiếp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Iterable, Tuple
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QInputDialog, QMessageBox, QWidget,
|
||||
)
|
||||
|
||||
from ..i18n import bind_text, tr
|
||||
|
||||
#: tên gọi trong ``**override`` -> (cờ nút chuẩn, khoá dịch mặc định)
|
||||
_STANDARD = {
|
||||
"save": (QDialogButtonBox.Save, "dialog.save"),
|
||||
"cancel": (QDialogButtonBox.Cancel, "dialog.cancel"),
|
||||
"ok": (QDialogButtonBox.Ok, "dialog.ok"),
|
||||
"close": (QDialogButtonBox.Close, "dialog.close"),
|
||||
}
|
||||
|
||||
|
||||
def dialog_buttons(standard, **override: str) -> QDialogButtonBox:
|
||||
"""``QDialogButtonBox`` có nhãn đổi theo ngôn ngữ đang chọn.
|
||||
|
||||
``override`` đổi khoá dịch của một nút cụ thể — ``ok="schedtask.ai_confirm"``
|
||||
cho hộp thoại muốn nút OK mang chữ riêng. Phải đi qua đây chứ không
|
||||
``setText(tr(...))`` sau khi dựng: lần đổi ngôn ngữ kế tiếp, ràng buộc bên
|
||||
dưới sẽ áp lại khoá mặc định và xoá mất chữ riêng đó.
|
||||
"""
|
||||
box = QDialogButtonBox(standard)
|
||||
for name, (flag, key) in _STANDARD.items():
|
||||
button = box.button(flag)
|
||||
if button is not None:
|
||||
bind_text(button, override.get(name, key))
|
||||
return box
|
||||
|
||||
|
||||
def confirm(parent: QWidget | None, title: str, body: str) -> bool:
|
||||
"""Hỏi Có/Không, trả về True nếu người dùng đồng ý.
|
||||
|
||||
Thay cho ``QMessageBox.question(...) == QMessageBox.Yes``, vốn hiện "&Yes" /
|
||||
"&No" tiếng Anh ở mọi ngôn ngữ. Hộp thoại dựng lại mỗi lần mở nên gọi
|
||||
``tr()`` một lần khi dựng là đủ, không cần ràng buộc.
|
||||
"""
|
||||
box = QMessageBox(QMessageBox.Question, title, body,
|
||||
QMessageBox.Yes | QMessageBox.No, parent)
|
||||
box.button(QMessageBox.Yes).setText(tr("dialog.yes"))
|
||||
box.button(QMessageBox.No).setText(tr("dialog.no"))
|
||||
return box.exec() == QMessageBox.Yes
|
||||
|
||||
|
||||
def _hoi(parent: QWidget | None, title: str, label: str,
|
||||
dung: Callable[[QInputDialog], None]) -> Tuple[str, bool]:
|
||||
"""Khung chung của ba hàm hỏi bên dưới; ``dung`` chỉnh riêng từng kiểu.
|
||||
|
||||
Các hàm tĩnh ``QInputDialog.get*`` tự dựng hộp thoại bên trong nên không
|
||||
chạm được vào nút của nó — phải tự dựng thì mới đặt được nhãn.
|
||||
"""
|
||||
dlg = QInputDialog(parent)
|
||||
dlg.setWindowTitle(title)
|
||||
dlg.setLabelText(label)
|
||||
dlg.setOkButtonText(tr("dialog.ok"))
|
||||
dlg.setCancelButtonText(tr("dialog.cancel"))
|
||||
dung(dlg)
|
||||
# exec() TRƯỚC, đọc giá trị SAU: gói cả hai vào một tuple thì Python tính
|
||||
# textValue() trước và trả về chữ có sẵn lúc chưa ai gõ gì.
|
||||
dong_y = dlg.exec() == QDialog.Accepted
|
||||
return dlg.textValue(), dong_y
|
||||
|
||||
|
||||
def ask_text(parent: QWidget | None, title: str, label: str,
|
||||
text: str = "") -> Tuple[str, bool]:
|
||||
"""Như ``QInputDialog.getText``, nhưng nút bấm theo ngôn ngữ đang chọn."""
|
||||
return _hoi(parent, title, label, lambda d: d.setTextValue(text))
|
||||
|
||||
|
||||
def ask_multiline(parent: QWidget | None, title: str, label: str,
|
||||
text: str = "") -> Tuple[str, bool]:
|
||||
"""Như ``QInputDialog.getMultiLineText``, nút bấm theo ngôn ngữ đang chọn."""
|
||||
def dung(d: QInputDialog) -> None:
|
||||
d.setOption(QInputDialog.UsePlainTextEditForTextInput)
|
||||
d.setTextValue(text)
|
||||
|
||||
return _hoi(parent, title, label, dung)
|
||||
|
||||
|
||||
def ask_item(parent: QWidget | None, title: str, label: str,
|
||||
items: Iterable[str], current: int = 0,
|
||||
editable: bool = False) -> Tuple[str, bool]:
|
||||
"""Như ``QInputDialog.getItem``, nút bấm theo ngôn ngữ đang chọn."""
|
||||
muc = list(items)
|
||||
|
||||
def dung(d: QInputDialog) -> None:
|
||||
d.setComboBoxItems(muc)
|
||||
d.setComboBoxEditable(editable)
|
||||
if muc:
|
||||
d.setTextValue(muc[current])
|
||||
|
||||
return _hoi(parent, title, label, dung)
|
||||
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from ..core.ext_connectors import CATEGORIES, PRESETS
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import dialog_buttons
|
||||
|
||||
# ms365 has no user-created entries here (see ConnectorsPanel) — it auto-connects
|
||||
# via its own built-in OneDrive/SharePoint toggles, so it's left off this picker.
|
||||
@@ -119,7 +120,7 @@ class ExtConnectorEditDialog(QDialog):
|
||||
|
||||
self._enabled = bool(connector.get("enabled", False))
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self._on_accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
lay.addWidget(buttons)
|
||||
|
||||
@@ -23,7 +23,7 @@ from PySide6.QtWidgets import (
|
||||
QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core import admin_agents
|
||||
from ..core import admin_agents, help_knowledge
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .icons import icon
|
||||
@@ -217,8 +217,7 @@ class HelpAgentWidget(QWidget):
|
||||
# ---- greeting / labels ------------------------------------------------
|
||||
def _greeting(self) -> str:
|
||||
"""Câu chào mở đầu, có tên người dùng nếu biết."""
|
||||
name = self._user_name or tr("help_agent.default_user")
|
||||
return tr("help_agent.greeting", name=name)
|
||||
return help_knowledge.greeting(self._user_name)
|
||||
|
||||
# ---- construction -----------------------------------------------------
|
||||
def _build_edge_tab(self) -> None:
|
||||
@@ -464,11 +463,12 @@ class HelpAgentWidget(QWidget):
|
||||
agent = admin_agents.ensure_help_agent(
|
||||
admin_agents.agents_admin_dir(self.ctx.config.shared_dir))
|
||||
history = list(self._history)
|
||||
sys_prompt = help_knowledge.build_prompt(agent.effective_prompt(), getattr(self.parent(), "help_context", lambda: "")())
|
||||
|
||||
def job(worker):
|
||||
"""Chạy nền: gọi provider của Help Agent kèm prompt hệ thống của nó."""
|
||||
provider = admin_agents.build_agent_provider(self.ctx, agent)
|
||||
messages = [{"role": "system", "content": agent.effective_prompt()}] + history
|
||||
messages = [{"role": "system", "content": sys_prompt}] + history
|
||||
result = provider.chat(messages, tools=None, cancel=worker.is_cancelled)
|
||||
content = result.get("content", "") if isinstance(result, dict) else str(result)
|
||||
return {"content": provider.strip_think(content) or ""}
|
||||
|
||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QSize, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QHBoxLayout, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..core import custom_icons
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from . import icons as icons_mod
|
||||
from .dialog_buttons import ask_multiline, ask_text
|
||||
from .icons import icon
|
||||
|
||||
|
||||
@@ -116,8 +117,8 @@ class IconsAdminTab(QWidget):
|
||||
path, _ = QFileDialog.getOpenFileName(self, tr("icons_admin.add"), "", "SVG (*.svg)")
|
||||
if not path:
|
||||
return
|
||||
name, ok = QInputDialog.getText(self, tr("icons_admin.name_prompt"),
|
||||
tr("icons_admin.name_prompt"))
|
||||
name, ok = ask_text(self, tr("icons_admin.name_prompt"),
|
||||
tr("icons_admin.name_prompt"))
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
@@ -129,12 +130,12 @@ class IconsAdminTab(QWidget):
|
||||
|
||||
def _add_from_svg_text(self) -> None:
|
||||
"""Thêm icon bằng cách dán thẳng mã SVG."""
|
||||
name, ok = QInputDialog.getText(self, tr("icons_admin.name_prompt"),
|
||||
tr("icons_admin.name_prompt"))
|
||||
name, ok = ask_text(self, tr("icons_admin.name_prompt"),
|
||||
tr("icons_admin.name_prompt"))
|
||||
if not ok or not name.strip():
|
||||
return
|
||||
svg, ok = QInputDialog.getMultiLineText(self, tr("icons_admin.paste"),
|
||||
tr("icons_admin.paste_prompt"))
|
||||
svg, ok = ask_multiline(self, tr("icons_admin.paste"),
|
||||
tr("icons_admin.paste_prompt"))
|
||||
if not ok:
|
||||
return
|
||||
try:
|
||||
|
||||
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import dialog_buttons
|
||||
|
||||
|
||||
class McpServerEditDialog(QDialog):
|
||||
@@ -41,7 +42,7 @@ class McpServerEditDialog(QDialog):
|
||||
self.args.setPlaceholderText(tr("mcp.args_placeholder"))
|
||||
lay.addWidget(self.args)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self._on_accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
lay.addWidget(buttons)
|
||||
|
||||
+12
-1
@@ -23,7 +23,7 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..i18n import tr
|
||||
from ..i18n import on_language_changed, tr
|
||||
|
||||
|
||||
class RoutingToggle(QWidget):
|
||||
@@ -71,6 +71,11 @@ class RoutingToggle(QWidget):
|
||||
self._label.setObjectName("hint")
|
||||
self._combo = QComboBox()
|
||||
self._combo.setToolTip(tr("routing.toggle_tooltip"))
|
||||
# Mode names differ in length per language ("Thủ công" is wider than
|
||||
# "手動"), and a combo only re-measures itself under this policy — without
|
||||
# it the width stays frozen at the language the widget was built in and
|
||||
# the longer translation is cut off.
|
||||
self._combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||||
# (data value, i18n key) — data is the persisted mode string. Order is
|
||||
# least-to-most autonomous, with Fallback (R03-T03) last because it is
|
||||
# the "only when something breaks" mode rather than a stronger Auto.
|
||||
@@ -88,6 +93,11 @@ class RoutingToggle(QWidget):
|
||||
self._combo.currentIndexChanged.connect(self._on_changed)
|
||||
lay.addWidget(self._label)
|
||||
lay.addWidget(self._combo)
|
||||
# Registered HERE, not by the four surfaces that embed this widget
|
||||
# (Cowork, Co4E, AI-Edit): every one of them had forgotten to, so the
|
||||
# control stayed frozen in the language it was built in. Owning it here
|
||||
# means no future embedder can forget either.
|
||||
on_language_changed(self.retranslate)
|
||||
|
||||
def current_mode(self) -> str:
|
||||
"""Chế độ định tuyến đang chọn; 'off' nếu chưa đặt."""
|
||||
@@ -149,6 +159,7 @@ class AutoRunToggle(QWidget):
|
||||
self.refresh()
|
||||
self._chk.toggled.connect(self._on_toggled)
|
||||
lay.addWidget(self._chk)
|
||||
on_language_changed(self.retranslate) # same reason as RoutingToggle
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Đọc lại trạng thái tự chạy của project đang mở lên ô đánh dấu."""
|
||||
|
||||
+54
-15
@@ -13,6 +13,8 @@ chưa từng được gán nên gọi vào là AttributeError.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -23,6 +25,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import dialog_buttons
|
||||
from .icons import IconLabel
|
||||
from .widgets import ToggleSwitch
|
||||
|
||||
@@ -31,6 +34,27 @@ from ..presentation.settings.general_settings_widget import GeneralSettingsWidge
|
||||
from ..presentation.settings.provider_settings_widget import ProviderSettingsWidget
|
||||
from ..presentation.settings.parameter_settings_widget import ParameterSettingsWidget
|
||||
from ..presentation.settings.routing_settings_widget import RoutingSettingsWidget
|
||||
from ..presentation.settings.about_widget import AboutSettingsWidget
|
||||
|
||||
|
||||
def _sandbox_password_matches(entered: str, stored: str) -> bool:
|
||||
"""Whether ``entered`` unlocks the Sandbox Security group.
|
||||
|
||||
An empty ``stored`` must never match. ``DEFAULT_CONFIG`` ships
|
||||
``agent_security.sandbox_pw = ""`` and the config handed to this dialog is
|
||||
always deep-merged with those defaults, so a plain ``entered == stored``
|
||||
accepts an empty field on every install that never set a password. The MS365
|
||||
unlock guards the same way — see ``json_config_repository.unlock_ms365``.
|
||||
|
||||
Both sides are compared as UTF-8 bytes, not as ``str``:
|
||||
``compare_digest`` raises ``TypeError`` on ``str`` holding anything outside
|
||||
ASCII, and this app defaults to Vietnamese and ships to Japanese customers,
|
||||
so an accented password is ordinary input rather than an edge case.
|
||||
"""
|
||||
if not entered or not stored:
|
||||
return False
|
||||
return secrets.compare_digest(entered.encode("utf-8"), stored.encode("utf-8"))
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
"""Hộp thoại Cài đặt: cột mục lục bên trái, các trang bên phải
|
||||
@@ -85,23 +109,23 @@ class SettingsDialog(QDialog):
|
||||
sbl = QVBoxLayout(self.sandbox_group)
|
||||
|
||||
# --- Password protection for Sandbox Security (at top) ---
|
||||
self.sandbox_pw_label = IconLabel("lock", "Sandbox Security Password")
|
||||
self.sandbox_pw_label = IconLabel("lock", tr("settings.sandbox_pw_label"))
|
||||
sbl.addWidget(self.sandbox_pw_label)
|
||||
|
||||
pw_row = QHBoxLayout()
|
||||
self.sandbox_pw_edit = QLineEdit("")
|
||||
self.sandbox_pw_edit.setPlaceholderText("Enter password to edit sandbox settings")
|
||||
self.sandbox_pw_edit.setPlaceholderText(tr("settings.sandbox_pw_placeholder"))
|
||||
self.sandbox_pw_edit.setEchoMode(QLineEdit.Password)
|
||||
pw_row.addWidget(self.sandbox_pw_edit, 1)
|
||||
self.sandbox_unlock_btn = QPushButton("Unlock")
|
||||
self.sandbox_unlock_btn = QPushButton(tr("settings.sandbox_unlock_btn"))
|
||||
self.sandbox_unlock_btn.clicked.connect(self._sandbox_unlock)
|
||||
pw_row.addWidget(self.sandbox_unlock_btn)
|
||||
self.sandbox_locked_status = IconLabel("lock", "Locked (changes disabled)", color="#c00")
|
||||
self.sandbox_locked_status = IconLabel("lock", tr("settings.sandbox_locked"), color="#c00")
|
||||
self.sandbox_locked_status.text_label().setStyleSheet("color: #c00; font-weight: bold;")
|
||||
pw_row.addWidget(self.sandbox_locked_status)
|
||||
sbl.addLayout(pw_row)
|
||||
self._sandbox_unlocked = False # Start LOCKED — must enter password first
|
||||
self._sandbox_pw = sec.get("sandbox_pw", "quandh14")
|
||||
self._sandbox_pw = sec.get("sandbox_pw", "")
|
||||
|
||||
# Separator line between pw section and sandbox settings
|
||||
pw_sep = QLabel("────────────────")
|
||||
@@ -122,15 +146,15 @@ class SettingsDialog(QDialog):
|
||||
# they belong with the other tool toggles — see ToolsAdminTab).
|
||||
|
||||
# --- Enable/Disable Agent Security ---
|
||||
self.sec_enabled = ToggleSwitch("Enable Agent Security (command validation)")
|
||||
self.sec_enabled = ToggleSwitch(tr("settings.sec_enabled"))
|
||||
self.sec_enabled.setChecked(bool(sec.get("enabled", True)))
|
||||
self.sec_enabled.setToolTip("Bật/tắt toàn bộ Agent Security")
|
||||
self.sec_enabled.setToolTip(tr("settings.sec_enabled_tooltip"))
|
||||
sbl.addWidget(self.sec_enabled)
|
||||
|
||||
# --- AI Command Check toggle ---
|
||||
self.ai_check = ToggleSwitch("AI check commands")
|
||||
self.ai_check = ToggleSwitch(tr("settings.ai_check"))
|
||||
self.ai_check.setChecked(bool(sec.get("command_ai_check", False)))
|
||||
self.ai_check.setToolTip("Cho AI control-agent xét lệnh trước khi chạy")
|
||||
self.ai_check.setToolTip(tr("settings.ai_check_tooltip"))
|
||||
sbl.addWidget(self.ai_check)
|
||||
|
||||
# Resource limits (CPU/Memory/Disk I/O) moved to the Parameter group
|
||||
@@ -169,12 +193,18 @@ class SettingsDialog(QDialog):
|
||||
from .widgets import section_panels
|
||||
|
||||
|
||||
# Giới thiệu đứng CUỐI: nó không có thiết lập nào để đổi, nên đặt trước
|
||||
# các mục thao tác được sẽ đẩy chúng xuống mà không được gì.
|
||||
self._about_page = AboutSettingsWidget(self.ctx)
|
||||
root.addWidget(self._about_page)
|
||||
|
||||
pages = []
|
||||
for label, widget in ((tr("settings.group.general"), self._general_box),
|
||||
(tr("settings.group.provider"), prov_group),
|
||||
(tr("settings.group.sandbox"), self.sandbox_group),
|
||||
(tr("settings.group.parameter"), param_group),
|
||||
(tr("routing.settings_group"), routing_group)):
|
||||
(tr("routing.settings_group"), routing_group),
|
||||
(tr("settings.group.about"), self._about_page)):
|
||||
root.removeWidget(widget)
|
||||
page = QWidget()
|
||||
pv = QVBoxLayout(page)
|
||||
@@ -200,7 +230,7 @@ class SettingsDialog(QDialog):
|
||||
widest = max(w.widget().sizeHint().width() for _lab, w in pages)
|
||||
self.setMinimumWidth(self.section_list.width() + widest + 60)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self._save)
|
||||
buttons.rejected.connect(self.reject)
|
||||
outer.addWidget(buttons)
|
||||
@@ -276,17 +306,26 @@ class SettingsDialog(QDialog):
|
||||
phải cơ chế bảo mật thật.
|
||||
"""
|
||||
pw = self.sandbox_pw_edit.text()
|
||||
if pw == self._sandbox_pw:
|
||||
if not self._sandbox_pw:
|
||||
# No password configured. Refusing with "wrong password" would be a
|
||||
# dead end — the user would keep retrying a password that cannot
|
||||
# exist — so name the actual state instead.
|
||||
QMessageBox.warning(self, tr("settings.sandbox_pw_unset_title"),
|
||||
tr("settings.sandbox_pw_unset_body"))
|
||||
return
|
||||
if _sandbox_password_matches(pw, self._sandbox_pw):
|
||||
self._sandbox_unlocked = True
|
||||
self.sandbox_locked_status.setText("Unlocked")
|
||||
self.sandbox_locked_status.setText(tr("settings.sandbox_unlocked"))
|
||||
self.sandbox_locked_status.set_icon("unlock", "#090")
|
||||
self.sandbox_locked_status.text_label().setStyleSheet("color: #090; font-weight: bold;")
|
||||
# Enable all sandbox widgets
|
||||
for w in self._sandbox_widgets:
|
||||
w.setEnabled(True)
|
||||
QMessageBox.information(self, "Sandbox Security", "Sandbox settings unlocked.")
|
||||
QMessageBox.information(self, tr("settings.group.sandbox"),
|
||||
tr("settings.sandbox_unlocked_body"))
|
||||
else:
|
||||
QMessageBox.warning(self, "Wrong Password", "Password incorrect. Sandbox settings remain locked.")
|
||||
QMessageBox.warning(self, tr("settings.sandbox_pw_wrong_title"),
|
||||
tr("settings.sandbox_pw_wrong_body"))
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Gom cấu hình từ mọi trang con rồi ghi xuống đĩa."""
|
||||
|
||||
+18
-10
@@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QApplication, QHBoxLayout, QInputDialog, QLabel,
|
||||
QLineEdit, QMenu, QMessageBox, QPushButton, QStyle, QStyledItemDelegate,
|
||||
QAbstractItemView, QApplication, QHBoxLayout, QLabel,
|
||||
QLineEdit, QMenu, QPushButton, QStyle, QStyledItemDelegate,
|
||||
QStyleOptionViewItem, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ from ..core.history import (
|
||||
rename_conversation, set_pinned,
|
||||
)
|
||||
from ..i18n import on_language_changed, tr
|
||||
from .dialog_buttons import ask_text, confirm
|
||||
from .icons import collapse_left_icon, dot_icon, DOT_BLUE, icon
|
||||
from .widgets import CollapseStrip
|
||||
from ..state import AppContext
|
||||
@@ -241,8 +242,17 @@ class HistorySidebar(QWidget):
|
||||
for p in projects:
|
||||
groups[p.project_id] = _make_group(p.name)
|
||||
|
||||
# Loc theo mot project -> doc dung thu muc cua no. Khong loc ("Tat ca
|
||||
# project…") -> phai doc thu muc lich su cua TUNG project roi gop lai:
|
||||
# lich su nam trong thu muc lam viec cua project, nen mot lan goi
|
||||
# list_conversations chi thay duoc project dang mo, va moi nhom con lai
|
||||
# hien ra rong tuy nguoi dung da chat trong do.
|
||||
try:
|
||||
convos = list_conversations(self.ctx.config.history_dir(), query=query)
|
||||
if self._project_filter:
|
||||
convos = list_conversations(self.ctx.config.history_dir(), query=query)
|
||||
else:
|
||||
from ..core.history import history_dirs, list_conversations_by_project
|
||||
convos = list_conversations_by_project(history_dirs(), query=query)
|
||||
except Exception:
|
||||
convos = []
|
||||
|
||||
@@ -337,15 +347,14 @@ class HistorySidebar(QWidget):
|
||||
set_pinned(path, not pinned)
|
||||
self.refresh()
|
||||
elif chosen == rename_act:
|
||||
new, ok = QInputDialog.getText(
|
||||
new, ok = ask_text(
|
||||
self, tr("sidebar.rename.title"), tr("sidebar.rename.label"), text=title)
|
||||
if ok and new.strip():
|
||||
rename_conversation(path, new.strip())
|
||||
self.refresh()
|
||||
elif chosen == del_act:
|
||||
if QMessageBox.question(
|
||||
self, tr("sidebar.delete.title"), tr("sidebar.delete.confirm", title=title)
|
||||
) == QMessageBox.Yes:
|
||||
if confirm(self, tr("sidebar.delete.title"),
|
||||
tr("sidebar.delete.confirm", title=title)):
|
||||
delete_conversation(path)
|
||||
self.refresh()
|
||||
self.history_changed.emit()
|
||||
@@ -365,9 +374,8 @@ class HistorySidebar(QWidget):
|
||||
"""Confirm, then delete every conversation in ``selected``. Split out
|
||||
of _bulk_delete_menu so tests can drive it directly without having to
|
||||
fake a real (modal, event-loop-blocking) QMenu popup."""
|
||||
if QMessageBox.question(
|
||||
self, tr("sidebar.delete.title"),
|
||||
tr("sidebar.delete_multi.confirm", n=len(selected))) != QMessageBox.Yes:
|
||||
if not confirm(self, tr("sidebar.delete.title"),
|
||||
tr("sidebar.delete_multi.confirm", n=len(selected))):
|
||||
return False
|
||||
for item in selected:
|
||||
path = item.data(0, Qt.UserRole)
|
||||
|
||||
@@ -12,7 +12,7 @@ from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QInputDialog, QLabel, QListWidget,
|
||||
QFileDialog, QHBoxLayout, QLabel, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ from ..core.skills import (
|
||||
)
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import ask_multiline
|
||||
from .icons import icon
|
||||
from .skills_dialog import SkillEditDialog
|
||||
|
||||
@@ -100,7 +101,7 @@ class SkillManagerTab(QWidget):
|
||||
QMessageBox.information(self, tr("skills.auto_generate_title"),
|
||||
tr("skills.auto_generate_unavailable"))
|
||||
return
|
||||
prompt, ok = QInputDialog.getMultiLineText(
|
||||
prompt, ok = ask_multiline(
|
||||
self, tr("skills.auto_generate_title"),
|
||||
tr("skills.auto_generate_prompt"), "")
|
||||
if not ok or not prompt.strip():
|
||||
|
||||
+4
-3
@@ -5,7 +5,7 @@ from typing import Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
|
||||
QDialog, QDialogButtonBox, QFileDialog, QHBoxLayout, QLabel,
|
||||
QLineEdit, QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit,
|
||||
QPushButton, QVBoxLayout,
|
||||
)
|
||||
@@ -17,6 +17,7 @@ from ..core.skills import (
|
||||
)
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import ask_multiline, dialog_buttons
|
||||
from .icons import icon
|
||||
|
||||
|
||||
@@ -58,7 +59,7 @@ class SkillEditDialog(QDialog):
|
||||
self.instr.setMinimumHeight(180)
|
||||
lay.addWidget(self.instr)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self._on_accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
lay.addWidget(buttons)
|
||||
@@ -192,7 +193,7 @@ class SkillsDialog(QDialog):
|
||||
QMessageBox.information(self, tr("skills.auto_generate_title"),
|
||||
tr("skills.auto_generate_unavailable"))
|
||||
return
|
||||
prompt, ok = QInputDialog.getMultiLineText(
|
||||
prompt, ok = ask_multiline(
|
||||
self, tr("skills.auto_generate_title"),
|
||||
tr("skills.auto_generate_prompt"), "")
|
||||
if not ok or not prompt.strip():
|
||||
|
||||
@@ -17,7 +17,7 @@ from typing import Dict, List, Optional
|
||||
from PySide6.QtCore import QDateTime, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox, QComboBox, QDateTimeEdit, QDialog, QDialogButtonBox, QFileDialog,
|
||||
QFormLayout, QGroupBox, QHBoxLayout, QInputDialog, QLabel, QLineEdit,
|
||||
QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||
QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton,
|
||||
QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
||||
)
|
||||
@@ -30,6 +30,7 @@ from ..core.tasks import (
|
||||
from ..core.projects import list_projects
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import tr
|
||||
from .dialog_buttons import ask_text, dialog_buttons
|
||||
from .icons import icon
|
||||
|
||||
# Only these two task types can be picked when adding/editing a task — Flow,
|
||||
@@ -451,7 +452,7 @@ class TaskEditorDialog(QDialog):
|
||||
# matching the five real group boxes, so you always see which group you
|
||||
# are in and how many are left. (Not tabs: the audit page asks for this
|
||||
# shape specifically, for consistency with Settings.)
|
||||
from .widgets import section_panels
|
||||
from .widgets import guard_wheel, section_panels
|
||||
|
||||
self._step_keys = ["schedtask.g_basic", "schedtask.g_schedule",
|
||||
"schedtask.g_input", "schedtask.g_dependency",
|
||||
@@ -481,7 +482,7 @@ class TaskEditorDialog(QDialog):
|
||||
widest = max(w.widget().sizeHint().width() for _lab, w in pages)
|
||||
self.setMinimumWidth(self.section_list.width() + widest + 60)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons = dialog_buttons(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
||||
buttons.button(QDialogButtonBox.Save).setIcon(icon("save"))
|
||||
buttons.button(QDialogButtonBox.Cancel).setIcon(icon("close"))
|
||||
buttons.accepted.connect(self._save)
|
||||
@@ -490,7 +491,6 @@ class TaskEditorDialog(QDialog):
|
||||
self._apply_hints()
|
||||
# Scrolling the form must never spin a combo/spin/date box the cursor
|
||||
# happens to hover — values only change after clicking into a field.
|
||||
from .widgets import guard_wheel
|
||||
guard_wheel(self)
|
||||
|
||||
def _retranslate_steps(self) -> None:
|
||||
@@ -540,8 +540,8 @@ class TaskEditorDialog(QDialog):
|
||||
|
||||
def _add_link(self) -> None:
|
||||
"""Thêm một link vào danh sách dữ liệu đầu vào của task."""
|
||||
url, ok = QInputDialog.getText(self, tr("schedtask.add_link_title"),
|
||||
tr("schedtask.add_link_label"))
|
||||
url, ok = ask_text(self, tr("schedtask.add_link_title"),
|
||||
tr("schedtask.add_link_label"))
|
||||
url = url.strip()
|
||||
if ok and url:
|
||||
self.links_list.addItem(url)
|
||||
@@ -607,7 +607,7 @@ class TaskEditorDialog(QDialog):
|
||||
candidates = self.all_tasks + ([self._original] if self._original else [self.task])
|
||||
err = chain_error(candidates, self.task["task_id"], self.next_combo.currentData())
|
||||
nxt_id = self.next_combo.currentData()
|
||||
warn = err or ""
|
||||
warn = tr(err) if err else ""
|
||||
if not err and nxt_id:
|
||||
nxt = next((t for t in self.all_tasks if t["task_id"] == nxt_id), None)
|
||||
if nxt and nxt.get("status") == "paused":
|
||||
@@ -719,12 +719,12 @@ class TaskEditorDialog(QDialog):
|
||||
err = chain_error(self.all_tasks + [self.task], self.task["task_id"],
|
||||
self.next_combo.currentData())
|
||||
if err:
|
||||
QMessageBox.warning(self, tr("schedtask.g_dependency"), err)
|
||||
QMessageBox.warning(self, tr("schedtask.g_dependency"), tr(err))
|
||||
return
|
||||
deps = self._checked_depends_on()
|
||||
err = depends_cycle_error(self.all_tasks + [self.task], self.task["task_id"], deps)
|
||||
if err:
|
||||
QMessageBox.warning(self, tr("schedtask.g_dependency"), err)
|
||||
QMessageBox.warning(self, tr("schedtask.g_dependency"), tr(err))
|
||||
return
|
||||
t = self.task
|
||||
t["title"] = title
|
||||
|
||||
+13
-75
@@ -21,38 +21,19 @@ from __future__ import annotations
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QMenu, QMessageBox, QPlainTextEdit, QPushButton, QSplitter, QTabWidget,
|
||||
QPlainTextEdit, QPushButton, QSplitter, QTabWidget,
|
||||
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..presentation.workspace.project_editing import ProjectEditingMixin, ProjectRow
|
||||
from ..state import AppContext
|
||||
from .icons import collapse_left_icon, icon
|
||||
from .osutil import open_folder
|
||||
from .widgets import CollapseStrip
|
||||
|
||||
|
||||
class _ProjectRow(QWidget):
|
||||
"""A project in the list: its name, and under it how much is in it.
|
||||
|
||||
The drawing gives every row a second line — "2 đoạn chat · 3 task" — which
|
||||
is the only thing on this screen that says a project holds anything at all.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, counts: str):
|
||||
"""Một dòng dự án trong danh sách: tên ở trên, số liệu tóm tắt ở dưới."""
|
||||
super().__init__()
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(6, 4, 6, 4)
|
||||
lay.setSpacing(0)
|
||||
title = QLabel(name)
|
||||
sub = QLabel(counts)
|
||||
sub.setObjectName("hint")
|
||||
lay.addWidget(title)
|
||||
lay.addWidget(sub)
|
||||
|
||||
|
||||
class WorkspaceTab(QWidget):
|
||||
class WorkspaceTab(ProjectEditingMixin, QWidget):
|
||||
"""Trang chủ Workspace: cột project, cột lịch sử, và 5 sub-tab
|
||||
(Dự án · Cowork · Co4E · Thư mục · GraphRAG).
|
||||
|
||||
@@ -265,6 +246,7 @@ class WorkspaceTab(QWidget):
|
||||
self._apply_pane_visibility()
|
||||
|
||||
self.refresh()
|
||||
self.install_project_editing()
|
||||
on_language_changed(self._retranslate)
|
||||
self._retranslate()
|
||||
|
||||
@@ -520,6 +502,7 @@ class WorkspaceTab(QWidget):
|
||||
self._cloud_sync_btn.setText(tr("workspace.cloud_sync"))
|
||||
self._refresh_cloud_badge()
|
||||
self._save_btn.setText(tr("workspace.save"))
|
||||
self.retranslate_project_rows()
|
||||
self._proj_collapse_btn.setToolTip(tr("workspace.collapse_projects_tooltip"))
|
||||
self._projects_strip.setToolTip(tr("workspace.expand_projects_tooltip"))
|
||||
self.tabs.setTabText(self._project_tab_idx, tr("workspace.tab_project"))
|
||||
@@ -637,7 +620,7 @@ class WorkspaceTab(QWidget):
|
||||
if p.description:
|
||||
item.setToolTip(p.description)
|
||||
self.project_list.addItem(item)
|
||||
row = _ProjectRow(p.name, tr("workspace.counts", chats=chats, tasks=tasks))
|
||||
row = ProjectRow(p.name, chats, tasks)
|
||||
item.setSizeHint(row.sizeHint())
|
||||
self.project_list.setItemWidget(item, row)
|
||||
if p.project_id == keep:
|
||||
@@ -652,11 +635,11 @@ class WorkspaceTab(QWidget):
|
||||
@staticmethod
|
||||
def _project_counts():
|
||||
"""{project_id: (chats, tasks)} — read once per refresh, not per row."""
|
||||
from ..core.history import list_conversations
|
||||
from ..core.history import history_dirs, list_conversations_by_project
|
||||
from ..core.tasks import list_tasks
|
||||
|
||||
out: dict = {}
|
||||
for conv in list_conversations():
|
||||
for conv in list_conversations_by_project(history_dirs()):
|
||||
pid = conv.get("project_id") or "default"
|
||||
chats, tasks = out.get(pid, (0, 0))
|
||||
out[pid] = (chats + 1, tasks)
|
||||
@@ -685,7 +668,7 @@ class WorkspaceTab(QWidget):
|
||||
|
||||
def _load_current(self) -> None:
|
||||
"""Nạp project đang chọn lên form và nối mọi sub-tab vào nó."""
|
||||
from ..core.projects import load_project
|
||||
from ..core.projects import load_project, project_history_dir
|
||||
|
||||
pid = self._selected_id()
|
||||
self._current_id = pid
|
||||
@@ -699,7 +682,7 @@ class WorkspaceTab(QWidget):
|
||||
# sharing that folder shares the history (another machine can view +
|
||||
# continue). No project → global history dir (attribute cleared).
|
||||
if project is not None:
|
||||
self.ctx.config._project_history_dir = project.workspace_dir() / ".cowork_history"
|
||||
self.ctx.config._project_history_dir = project_history_dir(project)
|
||||
else:
|
||||
self.ctx.config._project_history_dir = None
|
||||
if project is not None:
|
||||
@@ -710,7 +693,7 @@ class WorkspaceTab(QWidget):
|
||||
self._del_btn.setEnabled(True)
|
||||
self._reload_threads()
|
||||
if getattr(self, "_folder", None) is not None:
|
||||
self._folder.set_root(str(project.workspace_dir()))
|
||||
self._folder.set_project_root(str(project.workspace_dir()))
|
||||
else:
|
||||
self.name_edit.clear()
|
||||
self.desc_edit.clear()
|
||||
@@ -849,7 +832,7 @@ class WorkspaceTab(QWidget):
|
||||
self.ctx.active_project_id = pid or "default"
|
||||
self._refresh_mode_toggles()
|
||||
if self._structure is not None:
|
||||
self._structure.set_project(pid)
|
||||
self._structure.set_workspace_project(pid)
|
||||
if self._sidebar is not None:
|
||||
self._sidebar.set_project_filter(pid) # "" → show all (no project selected)
|
||||
# Route Co4E flow output into THIS project's workspace folder (so flow
|
||||
@@ -902,51 +885,6 @@ class WorkspaceTab(QWidget):
|
||||
self.threads.addTopLevelItem(item)
|
||||
|
||||
# ---- actions -----------------------------------------------------------
|
||||
def _create(self) -> None:
|
||||
"""Tạo project mới với tên mặc định rồi chọn nó."""
|
||||
from ..core.projects import new_project
|
||||
|
||||
project = new_project(tr("workspace.default_new_name"))
|
||||
self._current_id = project.project_id
|
||||
self.refresh()
|
||||
self.projects_changed.emit()
|
||||
self.name_edit.setFocus()
|
||||
self.name_edit.selectAll()
|
||||
|
||||
def _delete(self) -> None:
|
||||
"""Xoá project đang chọn sau khi hỏi xác nhận."""
|
||||
from ..core.projects import delete_project, load_project
|
||||
|
||||
pid = self._selected_id()
|
||||
project = load_project(pid) if pid else None
|
||||
if project is None:
|
||||
return
|
||||
if QMessageBox.question(
|
||||
self, tr("workspace.delete"),
|
||||
tr("workspace.delete_confirm", name=project.name)) != QMessageBox.Yes:
|
||||
return
|
||||
delete_project(pid)
|
||||
self._current_id = ""
|
||||
self.refresh() # empty workspace → Cowork/GraphRAG hidden until a new project
|
||||
self.projects_changed.emit()
|
||||
self.status_message.emit(tr("workspace.deleted", name=project.name))
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Lưu tên, mô tả và chỉ dẫn chung của project đang mở."""
|
||||
from ..core.projects import load_project, save_project
|
||||
|
||||
pid = self._current_id
|
||||
project = load_project(pid) if pid else None
|
||||
if project is None:
|
||||
return
|
||||
project.name = self.name_edit.text().strip() or project.name
|
||||
project.description = self.desc_edit.text().strip()
|
||||
project.instructions = self.instr_edit.toPlainText().strip()
|
||||
save_project(project)
|
||||
self.refresh()
|
||||
self.projects_changed.emit()
|
||||
self.status_message.emit(tr("workspace.saved", name=project.name))
|
||||
|
||||
def _pick_folder(self) -> None:
|
||||
"""Chọn thư mục sandbox cho project đang mở."""
|
||||
from ..core.projects import load_project, save_project
|
||||
@@ -966,7 +904,7 @@ class WorkspaceTab(QWidget):
|
||||
|
||||
def _open_workspace(self) -> None:
|
||||
"""Mở thư mục sandbox của project trong trình quản lý tệp của hệ điều hành."""
|
||||
from ..core.projects import load_project
|
||||
from ..core.projects import load_project, project_history_dir
|
||||
|
||||
project = load_project(self._current_id) if self._current_id else None
|
||||
if project is None:
|
||||
|
||||
Reference in New Issue
Block a user