Files
cowork-local/presentation/scheduling/ai_task_creator_dialog.py
T
Nam Pham Dinh ThanhandClaude Opus 5 7e11e9676d refactor: nốt 3 chỗ R08 còn thiếu — ChatPanel và 2 tab admin về đúng chỗ
Soát lại từng dòng plan thì thấy tôi báo R08 xong hơi sớm. Ba chỗ thiếu thật:

  T06  ChatPanel vẫn ở ui/, plan đòi presentation/chat/chat_panel.py
  T08  agents_admin_tab.py (498) và tools_admin_tab.py (245) vẫn ở ui/

    presentation/chat/chat_panel.py                    346
    presentation/monitoring/tabs/agents_admin_tab.py   383
    presentation/monitoring/tabs/agent_edit_dialog.py  143
    presentation/monitoring/tabs/tools_admin_tab.py    245
    ui/chat_panel.py / agents_admin_tab.py / tools_admin_tab.py  ~10 mỗi cái

agents_admin_tab.py 498 dòng nên tách thêm agent_edit_dialog.py: bảng danh
sách và hộp thoại sửa là hai việc, và hộp thoại còn tự đi hỏi provider xem có
model nào — thứ bảng không cần biết.

BA CHỖ CÒN LẠI KHÔNG PHẢI THIẾU, đã kiểm từng cái:
* audio_recorder_widget.py (T04) — repo KHÔNG có chức năng ghi âm nào.
* connector_settings_widget.py (T07) — UI Connector đã dời khỏi Cài đặt.
* sandbox_status_tab.py / mcp_history_tab.py (T08) — Hiệp đặt tên sandbox_tab
  và mcp_tab, nội dung đủ.

R08: 14/14 task, 0 file thiếu thật sự.
756 test xanh. 24/24 checker qua.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 01:24:45 +09:00

209 lines
9.1 KiB
Python

"""Hộp thoại tạo task bằng AI, và nhập task từ file — R08-T11.
Hai tab trong một hộp thoại vì cùng trả lời một câu: "làm sao có task mà
không phải điền tay từng ô".
* **Tạo bằng AI** — gõ một câu tiếng Việt, kèm được file và liên kết; AI sinh
ra cấu hình task và lịch chạy. Người dùng xem trước rồi mới xác nhận.
* **Nhập từ file** — xem ``ai_task_import_dialog.py``; phần nhập tách ra đó,
hộp thoại này chỉ đặt nó vào tab thứ hai.
"""
from __future__ import annotations
import copy
from pathlib import Path
from typing import Dict, List, Optional
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QAbstractItemView, QComboBox, QDialog, QDialogButtonBox, QHBoxLayout,
QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox,
QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget,
QTabBar, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
)
from ...core import tasks as taskrepo
from ...core.projects import list_projects
from ...core.tasks import STATUSES, chain_error, duplicate_task, new_task
from ...core.worker import AgentWorker
from ...i18n import on_language_changed, tr
from ...state import AppContext
from ...theme import current_palette
from ...ui.calendar_view import CalendarView
from ...ui.icons import icon
from ...ui.osutil import open_path
from .ai_task_import_dialog import TaskImportMixin
from .kanban_board_widget import _DropZone
class _AiCreateDialog(TaskImportMixin, 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 template (pick or drag)."""
def __init__(self, ctx: AppContext, parent=None):
super().__init__(parent)
from PySide6.QtWidgets import QTabWidget
self.ctx = ctx
self.created_tasks: List[dict] = []
self._planned: List[dict] = []
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)
# ---- tab 1: AI gen ------------------------------------------------
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)
self.tabs.addTab(ai_page, tr("schedtask.tab_ai"))
# ---- tab 2: Import from Excel --------------------------------------
imp_page = QWidget()
il = QVBoxLayout(imp_page)
tpl_btn = QPushButton(tr("schedtask.export_template_btn"))
tpl_btn.setIcon(icon("upload"))
tpl_btn.clicked.connect(self._export_template)
il.addWidget(tpl_btn)
pick_row = QHBoxLayout()
pick_btn = QPushButton(tr("schedtask.import_pick_btn"))
pick_btn.setIcon(icon("folder"))
pick_btn.clicked.connect(self._pick_import_file)
pick_row.addWidget(pick_btn)
pick_row.addStretch(1)
il.addLayout(pick_row)
self.drop_zone = _DropZone()
self.drop_zone.setText(tr("schedtask.drop_hint"))
self.drop_zone.file_dropped.connect(self._load_import_file)
il.addWidget(self.drop_zone)
il.addWidget(QLabel(tr("schedtask.ai_preview_label")))
self.import_preview = QPlainTextEdit()
self.import_preview.setReadOnly(True)
il.addWidget(self.import_preview, 1)
self.tabs.addTab(imp_page, tr("schedtask.tab_import"))
self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttons.button(QDialogButtonBox.Ok).setText(tr("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)
# ---- Import tab ------------------------------------------------------
def _ai_pick_files(self) -> None:
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]:
return [p.strip() for p in self.ai_files_edit.text().split(";") if p.strip()]
def _attached_links(self) -> List[str]:
return [u.strip() for u in self.ai_links_edit.text().split(";") if u.strip()]
def _generate(self) -> None:
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):
from ...core.ai_task_planner import plan_tasks
provider = self.ctx.build_active_provider()
full_desc = description
if files or links:
attach_note = "; ".join(files + links)
full_desc += f"\n\n(Attached references available: {attach_note})"
planned = plan_tasks(provider, full_desc, cancel=worker.is_cancelled)
# Attachments apply to every generated task so they're available
# at RUN time too, not just visible to the planner.
for t in planned:
t["input"]["file_paths"] = list(files)
t["input"]["links"] = list(links)
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:
self._worker = None
self.gen_btn.setEnabled(True)
self.gen_btn.setText(tr("schedtask.ai_generate"))
self._planned = result.get("tasks") or []
lines = []
for i, t in enumerate(self._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._planned))
def _on_failed(self, err: str) -> None:
self._worker = None
self.gen_btn.setEnabled(True)
self.gen_btn.setText(tr("schedtask.ai_generate"))
self.preview.setPlainText(str(err))
def _confirm(self) -> None:
project_id = self.workspace_combo.currentData() or ""
for t in self._planned:
t["project_id"] = project_id
self.created_tasks = self._planned
self.accept()