"""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()