CI / test (push) Canceled after 0s
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>
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"]
|