CI / test (pull_request) Canceled after 0s
Người dùng báo: chọn tiếng Nhật mà nhóm Sandbox Security, nút Save/Cancel và
nhiều chỗ khác vẫn tiếng Anh. Bộ test i18n cũ vẫn xanh vì nó chỉ bắt lỗi "có
dịch nhưng không ai áp lại" — hai lỗ thật nằm chỗ khác:
* Chuỗi HARDCODE không đi qua ``tr()`` bao giờ (``QPushButton("Unlock")``), nên
phép đo "đổi ngôn ngữ rồi tìm chỗ không mang mốc" thấy nó đứng yên ở cả hai
lần chụp và coi là bình thường.
* Nhãn nút do CHÍNH Qt vẽ. ``QDialogButtonBox``, ``QMessageBox.question`` và
``QInputDialog.get*`` lấy chữ từ bảng dịch riêng của 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 file
``qtbase_*.qm`` nào để cài — nên chúng luôn rơi về tiếng Anh.
``ui/dialog_buttons.py`` gán nhãn của dự án đè lên nhãn Qt: ``dialog_buttons``
(10 hộp thoại), ``confirm`` (13 hộp Có/Không), ``ask_text``/``ask_multiline``/
``ask_item`` (16 hộp nhập liệu). Cùng với 17 chuỗi hardcode và 5 câu lỗi mà
``core/tasks.py`` trả thẳng ra hộp thoại — nay trả KHOÁ i18n, nơi hiển thị mới
gọi ``tr()`` — là 61 chỗ.
Ba chỗ nữa cùng lớp lỗi, phát hiện khi rà lại:
* ``_add_section`` nhận chuỗi ĐÃ dịch nên bốn tiêu đề mục của Step config đứng
nguyên ở ngôn ngữ lúc dựng panel. Nay nhận khoá + ``bind_dynamic`` để không
mất trạng thái gập/mở khi đổi ngôn ngữ.
* Thẻ tool ở Giám sát ▸ Công cụ hiện thẳng ``spec.description`` — chuỗi gửi cho
MÔ HÌNH trong schema function-calling, phải giữ tiếng Anh. Thêm bộ mô tả hiển
thị riêng cho 9 tool.
* Tên nhóm catalog ở tab Connector ("Other (any generic MCP server)").
Kèm theo, phần giao diện người dùng yêu cầu:
* ``__version__`` 2.26.0 -> 0.0.1, một nguồn cho tiêu đề cửa sổ, tab Giới thiệu
và dòng mới ở góc phải thanh trạng thái (thay dòng ghi công tác giả).
* Tắt size grip: nó vẽ một vệt ngay bên phải dòng phiên bản. Cửa sổ vẫn kéo
giãn được từ các cạnh.
* ``_NAV_SETTINGS_GAP`` 10 -> 4: hàng Cài đặt bớt xa nhóm Dashboard/Giám sát.
Hai test SẼ TREO nếu không sửa kèm: chúng patch ``QInputDialog.getText/getItem``
để tự trả lời, mà code nay gọi ``ask_text``/``ask_item`` — patch không còn chặn
được và hộp thoại thật sẽ mở ra chờ người bấm.
Ba cổng mới trong ``tests/ui/test_i18n_khong_hardcode_chu.py`` canh ở mức cấu
trúc (không ai được dựng lại kiểu cũ); đã kiểm chúng CẮN trên bản trước khi sửa:
10 + 13 + 17 vi phạm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
214 lines
10 KiB
Python
214 lines
10 KiB
Python
"""AiTaskCreatorDialog — "AI Create Task" (R08-T11, extracted from
|
|
``ui/schedule_task_tab.py``'s ``_AiCreateDialog``, lines 579-641/722-794 of
|
|
the original 795-line file).
|
|
|
|
Still one dialog with two tabs (AI-gen, then Import — the latter is
|
|
:class:`~presentation.scheduling.ai_task_import_dialog.ImportTaskPanel`,
|
|
embedded here rather than duplicated): the physical file split matches
|
|
``docs/refactor/Feature_Architecture_Proposal.md``'s R08-T11 breakdown, the
|
|
user-visible dialog is unchanged. ``_confirm`` still uses "whichever tab
|
|
produced a task list most recently" (mirroring the original class's shared
|
|
``self._planned`` attribute) — the AI-gen tab sets it on completion, the
|
|
Import tab reports it through :attr:`ImportTaskPanel.tasks_changed`.
|
|
|
|
AI generation goes through
|
|
``application/scheduling/ai_task_planner_service.py::AiTaskPlannerService``
|
|
(R07-T05) instead of ``core.ai_task_planner.plan_tasks`` directly.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
|
|
from PySide6.QtWidgets import (
|
|
QComboBox, QDialog, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit,
|
|
QPlainTextEdit, QPushButton, QTabWidget, QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from cowork_local.application.scheduling.ai_task_planner_service import (
|
|
AiTaskPlannerService,
|
|
)
|
|
from cowork_local.core.projects import list_projects
|
|
from cowork_local.core.worker import AgentWorker
|
|
from cowork_local.i18n import tr
|
|
from cowork_local.presentation.scheduling.ai_task_import_dialog import ImportTaskPanel
|
|
from cowork_local.state import AppContext
|
|
from cowork_local.ui.dialog_buttons import dialog_buttons
|
|
from cowork_local.ui.icons import icon
|
|
|
|
|
|
class AiTaskCreatorDialog(QDialog):
|
|
"""Create tasks two ways, one tab each (both preview first — nothing is
|
|
saved until the user confirms): ✨ AI gen from a natural-language
|
|
description, or 📥 Import from a filled Excel/CSV/JSON file."""
|
|
|
|
def __init__(self, ctx: AppContext, parent=None):
|
|
"""Hộp thoại tạo task bằng AI hoặc nhập từ tệp.
|
|
|
|
Nhớ tab nào đang cấp danh sách task cho nút Đồng ý — hai tab cùng sinh ra
|
|
danh sách, không phân biệt thì bấm Đồng ý có thể tạo nhầm danh sách của tab
|
|
kia.
|
|
"""
|
|
super().__init__(parent)
|
|
self.ctx = ctx
|
|
self._planner = AiTaskPlannerService(provider_factory=ctx.build_active_provider)
|
|
self.created_tasks: List[dict] = []
|
|
self._ai_planned: List[dict] = []
|
|
# Which tab produced the task list currently backing the Ok button —
|
|
# mirrors the original single-class dialog's shared `self._planned`
|
|
# attribute, where whichever of _on_planned()/_load_import_file()
|
|
# ran LAST (regardless of which tab is currently showing) won.
|
|
self._active_source = "ai"
|
|
self._worker: Optional[AgentWorker] = None
|
|
self.setWindowTitle(tr("schedtask.ai_btn"))
|
|
self.resize(600, 520)
|
|
|
|
root = QVBoxLayout(self)
|
|
ws_row = QHBoxLayout()
|
|
ws_row.addWidget(QLabel(tr("schedtask.f_workspace")))
|
|
self.workspace_combo = QComboBox()
|
|
self.workspace_combo.addItem(tr("schedtask.no_workspace"), "")
|
|
for p in list_projects():
|
|
self.workspace_combo.addItem(p.name, p.project_id)
|
|
self.workspace_combo.setToolTip(tr("schedtask.hint_workspace"))
|
|
ws_row.addWidget(self.workspace_combo, 1)
|
|
root.addLayout(ws_row)
|
|
self.tabs = QTabWidget()
|
|
root.addWidget(self.tabs, 1)
|
|
|
|
self.tabs.addTab(self._build_ai_gen_page(), tr("schedtask.tab_ai"))
|
|
self.import_panel = ImportTaskPanel(self._planner)
|
|
self.import_panel.tasks_changed.connect(self._on_import_tasks_changed)
|
|
self.tabs.addTab(self.import_panel, tr("schedtask.tab_import"))
|
|
|
|
self.buttons = dialog_buttons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
|
|
ok="schedtask.ai_confirm")
|
|
self.buttons.button(QDialogButtonBox.Ok).setEnabled(False)
|
|
self.buttons.accepted.connect(self._confirm)
|
|
self.buttons.rejected.connect(self.reject)
|
|
root.addWidget(self.buttons)
|
|
|
|
# ---- AI-gen tab -------------------------------------------------------
|
|
def _build_ai_gen_page(self) -> QWidget:
|
|
"""Dựng tab "Sinh bằng AI": ô mô tả, tệp/link đính kèm và nút sinh."""
|
|
ai_page = QWidget()
|
|
al = QVBoxLayout(ai_page)
|
|
al.addWidget(QLabel(tr("schedtask.ai_desc_label")))
|
|
self.desc_edit = QPlainTextEdit()
|
|
self.desc_edit.setPlaceholderText(tr("schedtask.ai_desc_ph"))
|
|
self.desc_edit.setMaximumHeight(110)
|
|
al.addWidget(self.desc_edit)
|
|
# Attachments (files + links) — merged into every task this generates,
|
|
# AND into the planning prompt so the AI knows they exist.
|
|
attach_row = QHBoxLayout()
|
|
self.ai_files_edit = QLineEdit()
|
|
self.ai_files_edit.setPlaceholderText(tr("schedtask.files_placeholder"))
|
|
ai_pick_btn = QPushButton(tr("schedtask.pick_files"))
|
|
ai_pick_btn.setIcon(icon("folder"))
|
|
ai_pick_btn.clicked.connect(self._ai_pick_files)
|
|
attach_row.addWidget(self.ai_files_edit, 1)
|
|
attach_row.addWidget(ai_pick_btn)
|
|
al.addWidget(QLabel(tr("schedtask.f_files")))
|
|
al.addLayout(attach_row)
|
|
self.ai_links_edit = QLineEdit()
|
|
self.ai_links_edit.setPlaceholderText(tr("schedtask.links_placeholder"))
|
|
al.addWidget(QLabel(tr("schedtask.f_links")))
|
|
al.addWidget(self.ai_links_edit)
|
|
self.gen_btn = QPushButton(tr("schedtask.ai_generate"))
|
|
self.gen_btn.setIcon(icon("sparkle"))
|
|
self.gen_btn.setObjectName("primary")
|
|
self.gen_btn.clicked.connect(self._generate)
|
|
al.addWidget(self.gen_btn)
|
|
al.addWidget(QLabel(tr("schedtask.ai_preview_label")))
|
|
self.preview = QPlainTextEdit()
|
|
self.preview.setReadOnly(True)
|
|
al.addWidget(self.preview, 1)
|
|
return ai_page
|
|
|
|
def _ai_pick_files(self) -> None:
|
|
"""Chọn tệp đính kèm làm ngữ cảnh cho AI lập kế hoạch."""
|
|
from PySide6.QtWidgets import QFileDialog
|
|
|
|
files, _ = QFileDialog.getOpenFileNames(self, tr("schedtask.pick_files"))
|
|
if files:
|
|
existing = [f for f in self.ai_files_edit.text().split(";") if f.strip()]
|
|
self.ai_files_edit.setText("; ".join(existing + files))
|
|
|
|
def _attached_files(self) -> List[str]:
|
|
"""Danh sách đường dẫn tệp đã nhập, tách theo dấu ``;``."""
|
|
return [p.strip() for p in self.ai_files_edit.text().split(";") if p.strip()]
|
|
|
|
def _attached_links(self) -> List[str]:
|
|
"""Danh sách link đã nhập, tách theo dấu ``;``."""
|
|
return [u.strip() for u in self.ai_links_edit.text().split(";") if u.strip()]
|
|
|
|
def _generate(self) -> None:
|
|
"""Nhờ AI tách mô tả thành nhiều task; đang chạy dở thì bỏ qua."""
|
|
description = self.desc_edit.toPlainText().strip()
|
|
if not description or self._worker is not None:
|
|
return
|
|
files, links = self._attached_files(), self._attached_links()
|
|
self.gen_btn.setEnabled(False)
|
|
self.gen_btn.setText(tr("schedtask.ai_generating"))
|
|
|
|
def job(worker: AgentWorker):
|
|
"""Chạy nền: ghép mô tả với danh sách tệp/link rồi gọi bộ lập kế hoạch."""
|
|
full_desc = description
|
|
if files or links:
|
|
attach_note = "; ".join(files + links)
|
|
full_desc += f"\n\n(Attached references available: {attach_note})"
|
|
planned = self._planner.plan(
|
|
full_desc, file_paths=files, links=links, cancel=worker.is_cancelled)
|
|
return {"tasks": planned}
|
|
|
|
w = AgentWorker(job)
|
|
w.finished_ok.connect(self._on_planned)
|
|
w.failed.connect(self._on_failed)
|
|
self._worker = w
|
|
w.start()
|
|
|
|
def _on_planned(self, result: dict) -> None:
|
|
"""Có kết quả: đổ danh sách task đề xuất ra bảng để người dùng xem lại."""
|
|
self._worker = None
|
|
self.gen_btn.setEnabled(True)
|
|
self.gen_btn.setText(tr("schedtask.ai_generate"))
|
|
self._ai_planned = result.get("tasks") or []
|
|
self._active_source = "ai"
|
|
lines = []
|
|
for i, t in enumerate(self._ai_planned, 1):
|
|
sched = t.get("schedule", {})
|
|
when = sched.get("run_at") if sched.get("enabled") else tr("schedtask.no_schedule")
|
|
dep = t.get("dependency", {})
|
|
chain = f" ← {dep.get('previous_task_id', '')[:8]}" if dep.get("previous_task_id") else ""
|
|
lines.append(f"{i}. [{t.get('task_type')}] {t.get('title')}\n"
|
|
f" {when} repeat={sched.get('repeat_type', 'none')}{chain}\n"
|
|
f" {t.get('description', '')[:150]}")
|
|
self.preview.setPlainText("\n\n".join(lines))
|
|
self.buttons.button(QDialogButtonBox.Ok).setEnabled(bool(self._ai_planned))
|
|
|
|
def _on_failed(self, err: str) -> None:
|
|
"""Lập kế hoạch lỗi: trả nút về trạng thái bấm được và hiện lý do."""
|
|
self._worker = None
|
|
self.gen_btn.setEnabled(True)
|
|
self.gen_btn.setText(tr("schedtask.ai_generate"))
|
|
self.preview.setPlainText(str(err))
|
|
|
|
# ---- Import tab ---------------------------------------------------------
|
|
def _on_import_tasks_changed(self, has_tasks: bool) -> None:
|
|
"""Tab Nhập có task thì chuyển nguồn dữ liệu sang đó và mở nút OK."""
|
|
if has_tasks:
|
|
self._active_source = "import"
|
|
self.buttons.button(QDialogButtonBox.Ok).setEnabled(has_tasks)
|
|
|
|
# ---- confirm ------------------------------------------------------------
|
|
def _confirm(self) -> None:
|
|
"""Chốt: lấy task từ nguồn đang hoạt động (AI hoặc Nhập) và gắn project cho chúng."""
|
|
planned = self.import_panel.planned if self._active_source == "import" else self._ai_planned
|
|
project_id = self.workspace_combo.currentData() or ""
|
|
for t in planned:
|
|
t["project_id"] = project_id
|
|
self.created_tasks = planned
|
|
self.accept()
|
|
|
|
|
|
__all__ = ["AiTaskCreatorDialog"]
|