"""Tạo, sửa, đổi tên, xoá, nhân bản workflow — R08-T09. Chỉ thao tác trên danh sách. Phần chạy một workflow nằm ở ``co4e_runs.py``, phần vẽ node nằm ở canvas. """ from __future__ import annotations import re from typing import List, Optional from PySide6.QtCore import QSize, Qt from PySide6.QtWidgets import QInputDialog, QMenu from ...core import co4e from ...i18n import tr from ...ui.icons import icon from ...presentation.co4e.co4e_chat_view import _skill_names class Co4EWorkflowCrudMixin: def _apply_workflow(self, wf: co4e.Workflow) -> None: self._wf = wf # Per-flow outputs are kept in self._flow_outputs[wf.id] — do NOT clear # here (switching tabs must not wipe another flow's accumulated context). # Switch the visible conversation to THIS flow's own log. self.chat_stack.setCurrentWidget(self._ensure_flow_log(wf.id)) self.name_edit.setText(wf.name) self.canvas.load(wf.nodes, wf.edges) self.config.clear_step() if wf.nodes: self.canvas.relayout_if_vertical() # convert old top-down flows to left→right self.canvas.fit_view() self._update_run_btn() # reflect THIS flow's run state self._refresh_usage_total() # show THIS flow's token/cost total def _new_workflow(self) -> None: self._open_flow(co4e.new_workflow(tr("co4e.untitled"))) # Pressing this while the canvas already holds an empty untitled flow # produced an identical empty untitled flow — correct, and completely # invisible, so the button read as broken. Say what happened and put the # cursor where the next thing to do is: naming it. self.name_edit.setFocus() self.name_edit.selectAll() self.status_message.emit(tr("co4e.new_flow_ready")) def _selected_wf(self) -> Optional[co4e.Workflow]: """Materialise the selected saved-flow row into a Workflow.""" item = self.wf_list.currentItem() if item is None: return None _kind, ident = item.data(Qt.UserRole) return co4e.get_workflow(ident) def _load_selected_workflow(self, *_a) -> None: wf = self._selected_wf() if wf is not None: self._open_flow(wf) # open (or focus) its browser-style tab def _edit_selected_workflow(self) -> None: wf = self._selected_wf() if wf is None: self.status_message.emit(tr("co4e.select_flow")) return self._open_flow(wf) def _duplicate_selected_workflow(self) -> None: wf = self._selected_wf() if wf is None: self.status_message.emit(tr("co4e.select_flow")) return dup = co4e.duplicate_workflow(wf) self._reload_sidebar() self.status_message.emit(tr("co4e.duplicated_msg", name=dup.name)) def _wf_context_menu(self, pos) -> None: lw = self.wf_list item = lw.itemAt(pos) if item is None: return lw.setCurrentItem(item) _kind, ident = item.data(Qt.UserRole) menu = QMenu(lw) act_edit = menu.addAction(icon("edit"), tr("co4e.edit")) act_rename = menu.addAction(icon("edit"), tr("co4e.rename")) act_dup = menu.addAction(icon("branch"), tr("co4e.duplicate")) act_run = menu.addAction(icon("play"), tr("co4e.run_bg")) act_del = menu.addAction(icon("trash"), tr("co4e.delete")) chosen = menu.exec(lw.viewport().mapToGlobal(pos)) if chosen is act_edit: self._edit_selected_workflow() elif chosen is act_rename: self._rename_workflow(ident) elif chosen is act_dup: self._duplicate_selected_workflow() elif chosen is act_run: self._run_selected_in_background() elif chosen is act_del: self._delete_selected_workflow() def _rename_workflow(self, ident: str) -> None: """Rename a saved flow in place (e.g. to match its function/task).""" wf = co4e.get_workflow(ident) if wf is None: return name, ok = QInputDialog.getText(self, tr("co4e.rename"), tr("co4e.rename_prompt"), text=wf.name) name = (name or "").strip() if not ok or not name: return wf.name = name co4e.save_workflow(wf) if self._wf.id == ident: self.name_edit.setText(name) self._wf.name = name self._reload_sidebar() self.status_message.emit(tr("co4e.renamed_msg", name=name)) def _delete_selected_workflow(self) -> None: item = self.wf_list.currentItem() if item is None: return _kind, ident = item.data(Qt.UserRole) co4e.delete_workflow(ident) self._reload_sidebar() def _sync_wf_from_canvas(self) -> None: self._wf.nodes = self.canvas.nodes() self._wf.edges = self.canvas.edges() self._wf.name = self.name_edit.text().strip() or tr("co4e.untitled") def _save(self, as_template: bool) -> None: self._sync_wf_from_canvas() self._wf.is_template = as_template co4e.save_workflow(self._wf) self._reload_sidebar() self.status_message.emit(tr("co4e.saved_msg", name=self._wf.name)) def _autosave(self) -> None: if co4e.get_workflow(self._wf.id) is not None: self._sync_wf_from_canvas() co4e.save_workflow(self._wf) def _on_name_changed(self, text: str) -> None: self._wf.name = text.strip() or tr("co4e.untitled") self._sync_active_flow_tab_text() def _add_blank_step(self) -> None: self.canvas.add_palette_step(co4e.Step(label="New Step"), self.canvas.mapToScene(self.canvas.rect().center())) def _on_node_selected(self, node_id: str) -> None: for n in self.canvas.nodes(): if n.id == node_id: self.config.load_step(node_id, n.data, _skill_names()) if self._config_collapsed: self._toggle_config() return def _on_config_changed(self) -> None: for n in self.canvas.nodes(): self.canvas.refresh_node(n.id) self._autosave() def _wf_by_id(self, wf_id: str) -> Optional[co4e.Workflow]: """Resolve a flow id to a Workflow — saved, or the open canvas.""" wf = co4e.get_workflow(wf_id) if wf is not None: return wf if self._wf.id == wf_id: self._sync_wf_from_canvas() return self._wf return None