Files
cowork-local/presentation/co4e/co4e_workflow_crud.py
T

196 lines
9.4 KiB
Python

"""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 QMenu
from ...core import co4e
from ...core.co4e import STEP_DONE, STEP_RUNNING
from ...i18n import tr
from ...ui.dialog_buttons import ask_text
from ...ui.icons import icon
from ...presentation.co4e.co4e_chat_view import _skill_names
_LOCKED_NODE_STATUSES = (STEP_RUNNING, STEP_DONE)
class Co4EWorkflowCrudMixin:
"""Phần tạo/mở/lưu/xoá luồng của Co4E Studio.
Tách khỏi ``Co4ETab`` vì đây là nhóm việc trên *danh sách luồng đã lưu*,
không phải trên khung vẽ đang mở — dù hai bên nói chuyện với nhau liên tục.
"""
def _apply_workflow(self, wf: co4e.Workflow) -> None:
"""Đưa một luồng lên khung vẽ và chuyển hội thoại sang nhật ký của chính nó.
KHÔNG xoá ``_flow_outputs``: mỗi luồng giữ ngữ cảnh tích luỹ riêng, đổi
tab mà xoá là mất kết quả của luồng khác. Luồng cũ xếp dọc thì tự bẻ lại
thành trái→phải cho khớp bố cục hiện tại.
"""
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:
"""Tạo luồng trống mới, đưa con trỏ vào ô tên và báo trạng thái.
Trước đây bấm nút này khi khung vẽ đã có sẵn một luồng trống chưa đặt tên
sẽ tạo ra một luồng trống y hệt — đúng logic nhưng nhìn không thấy gì đổi,
nên nút bị hiểu là hỏng. Giờ nói rõ vừa xảy ra chuyện gì và đặt con trỏ
vào đúng việc tiếp theo: đặt tên.
"""
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:
"""Mở luồng đang chọn trong danh sách (hoặc chuyển sang tab của nó nếu đã mở)."""
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:
"""Mở luồng đang chọn để sửa; chưa chọn gì thì nhắc người dùng chọn."""
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:
"""Nhân bản luồng đang chọn thành một luồng mới rồi nạp lại danh sách."""
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:
"""Menu chuột phải trên danh sách luồng: sửa, đổi tên, nhân bản, chạy nền, xoá."""
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 = ask_text(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:
"""Xoá luồng đang chọn khỏi đĩa rồi nạp lại danh sách."""
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:
"""Chép node, cạnh và tên từ khung vẽ về đối tượng luồng trước khi lưu."""
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:
"""Lưu luồng đang mở; ``as_template`` đánh dấu nó là mẫu dùng lại được."""
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:
"""Tự lưu sau mỗi thay đổi trên khung vẽ — nhưng chỉ với luồng ĐÃ tồn tại
trên đĩa, để luồng nháp chưa đặt tên không tự nhảy vào danh sách.
"""
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:
"""Gõ tên mới thì cập nhật luôn nhãn tab của luồng đang mở."""
self._wf.name = text.strip() or tr("co4e.untitled")
self._sync_active_flow_tab_text()
def _add_blank_step(self) -> None:
"""Thêm một bước trống vào giữa khung nhìn hiện tại."""
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:
"""Chọn một node thì nạp bước đó vào bảng thuộc tính, tự mở bảng nếu đang gập.
Bước đang chạy hoặc đã chạy xong thì khoá ô nhập liệu ngay khi nạp —
tránh sửa nhầm cấu hình của lần chạy đang xem kết quả.
"""
for n in self.canvas.nodes():
if n.id == node_id:
self.config.load_step(node_id, n.data, _skill_names())
self.config.set_locked(self.canvas.node_status(node_id) in _LOCKED_NODE_STATUSES)
if self._config_collapsed:
self._toggle_config()
return
def _on_config_changed(self) -> None:
"""Sửa thuộc tính bước thì vẽ lại mọi node (nhãn/model có thể đổi) rồi tự lưu."""
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