Add flow visualization for tool builder
Implements node-based flow visualization using NodeGraphQt-QuiltiX-fork: New files: - src/cmdforge/gui/widgets/flow_graph.py: FlowGraphWidget with custom node types - InputNode: Shows tool inputs ($input + argument variables) - PromptNode: AI prompt steps with provider display - CodeNode: Python code steps - OutputNode: Final output - scripts/test_nodegraph.py: Standalone prototype for testing Modified files: - tool_builder_page.py: Added List/Flow view toggle with QStackedWidget - Lazy-loads flow widget on first use - Syncs between list and flow views - Double-click nodes to edit steps - styles.py: Added viewToggle button styling - widgets/__init__.py: Export FlowGraphWidget - pyproject.toml: Added 'flow' optional dependency group Features: - Toggle between List and Flow views in Steps panel - Nodes auto-connected based on step order - Auto-layout and fit-to-view on load - Double-click nodes opens step edit dialog - Graceful fallback if NodeGraphQt not installed Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -4,7 +4,8 @@ from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,
|
||||
QLineEdit, QTextEdit, QComboBox, QPushButton,
|
||||
QGroupBox, QListWidget, QListWidgetItem, QLabel,
|
||||
QMessageBox, QSplitter, QFrame
|
||||
QMessageBox, QSplitter, QFrame, QStackedWidget,
|
||||
QButtonGroup
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
@@ -23,6 +24,7 @@ class ToolBuilderPage(QWidget):
|
||||
self.editing = tool_name is not None
|
||||
self.original_name = tool_name
|
||||
self._tool = None
|
||||
self._flow_widget = None # Lazy-loaded
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
@@ -123,14 +125,57 @@ class ToolBuilderPage(QWidget):
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(16)
|
||||
|
||||
# Steps group
|
||||
# Steps group with view toggle
|
||||
steps_box = QGroupBox("Steps")
|
||||
steps_layout = QVBoxLayout(steps_box)
|
||||
|
||||
# View toggle header
|
||||
view_header = QHBoxLayout()
|
||||
|
||||
self.btn_list_view = QPushButton("List")
|
||||
self.btn_list_view.setCheckable(True)
|
||||
self.btn_list_view.setChecked(True)
|
||||
self.btn_list_view.setObjectName("viewToggle")
|
||||
self.btn_list_view.clicked.connect(lambda: self._set_view_mode(0))
|
||||
|
||||
self.btn_flow_view = QPushButton("Flow")
|
||||
self.btn_flow_view.setCheckable(True)
|
||||
self.btn_flow_view.setObjectName("viewToggle")
|
||||
self.btn_flow_view.clicked.connect(lambda: self._set_view_mode(1))
|
||||
|
||||
# Button group for mutual exclusivity
|
||||
self.view_group = QButtonGroup(self)
|
||||
self.view_group.addButton(self.btn_list_view, 0)
|
||||
self.view_group.addButton(self.btn_flow_view, 1)
|
||||
|
||||
view_header.addWidget(self.btn_list_view)
|
||||
view_header.addWidget(self.btn_flow_view)
|
||||
view_header.addStretch()
|
||||
steps_layout.addLayout(view_header)
|
||||
|
||||
# Stacked widget for list/flow views
|
||||
self.steps_stack = QStackedWidget()
|
||||
|
||||
# List view (index 0)
|
||||
list_container = QWidget()
|
||||
list_layout = QVBoxLayout(list_container)
|
||||
list_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.steps_list = QListWidget()
|
||||
self.steps_list.itemDoubleClicked.connect(self._edit_step)
|
||||
steps_layout.addWidget(self.steps_list)
|
||||
list_layout.addWidget(self.steps_list)
|
||||
|
||||
self.steps_stack.addWidget(list_container)
|
||||
|
||||
# Flow view placeholder (index 1) - lazy loaded
|
||||
flow_placeholder = QLabel("Loading flow visualization...")
|
||||
flow_placeholder.setAlignment(Qt.AlignCenter)
|
||||
flow_placeholder.setStyleSheet("color: #718096; padding: 40px;")
|
||||
self.steps_stack.addWidget(flow_placeholder)
|
||||
|
||||
steps_layout.addWidget(self.steps_stack)
|
||||
|
||||
# Step action buttons
|
||||
steps_btns = QHBoxLayout()
|
||||
self.btn_add_prompt = QPushButton("Add Prompt")
|
||||
self.btn_add_prompt.clicked.connect(self._add_prompt_step)
|
||||
@@ -173,6 +218,52 @@ class ToolBuilderPage(QWidget):
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
def _set_view_mode(self, mode: int):
|
||||
"""Switch between list (0) and flow (1) views."""
|
||||
if mode == 1 and self._flow_widget is None:
|
||||
# Lazy-load flow widget
|
||||
self._init_flow_widget()
|
||||
|
||||
self.steps_stack.setCurrentIndex(mode)
|
||||
|
||||
# Update flow widget if visible
|
||||
if mode == 1 and self._flow_widget:
|
||||
self._flow_widget.set_tool(self._tool)
|
||||
|
||||
def _init_flow_widget(self):
|
||||
"""Initialize the flow visualization widget."""
|
||||
try:
|
||||
from ..widgets.flow_graph import FlowGraphWidget
|
||||
self._flow_widget = FlowGraphWidget()
|
||||
self._flow_widget.node_double_clicked.connect(self._on_flow_node_double_clicked)
|
||||
|
||||
# Replace placeholder
|
||||
old_widget = self.steps_stack.widget(1)
|
||||
self.steps_stack.removeWidget(old_widget)
|
||||
old_widget.deleteLater()
|
||||
self.steps_stack.insertWidget(1, self._flow_widget)
|
||||
|
||||
# Set current tool
|
||||
if self._tool:
|
||||
self._flow_widget.set_tool(self._tool)
|
||||
except ImportError as e:
|
||||
# NodeGraphQt not available
|
||||
error_label = QLabel(f"Flow visualization unavailable:\n{e}")
|
||||
error_label.setAlignment(Qt.AlignCenter)
|
||||
error_label.setStyleSheet("color: #e53e3e; padding: 40px;")
|
||||
old_widget = self.steps_stack.widget(1)
|
||||
self.steps_stack.removeWidget(old_widget)
|
||||
old_widget.deleteLater()
|
||||
self.steps_stack.insertWidget(1, error_label)
|
||||
|
||||
def _on_flow_node_double_clicked(self, step_index: int, step_type: str):
|
||||
"""Handle double-click on a flow node."""
|
||||
if not self._tool or step_index < 0 or step_index >= len(self._tool.steps):
|
||||
return
|
||||
|
||||
step = self._tool.steps[step_index]
|
||||
self._edit_step_at_index(step_index, step)
|
||||
|
||||
def _load_tool(self, name: str):
|
||||
"""Load an existing tool for editing."""
|
||||
tool = load_tool(name)
|
||||
@@ -213,7 +304,7 @@ class ToolBuilderPage(QWidget):
|
||||
self.args_list.addItem(item)
|
||||
|
||||
def _refresh_steps(self):
|
||||
"""Refresh steps list."""
|
||||
"""Refresh steps list and flow view."""
|
||||
self.steps_list.clear()
|
||||
if self._tool and self._tool.steps:
|
||||
for i, step in enumerate(self._tool.steps, 1):
|
||||
@@ -227,6 +318,10 @@ class ToolBuilderPage(QWidget):
|
||||
item.setData(Qt.UserRole, step)
|
||||
self.steps_list.addItem(item)
|
||||
|
||||
# Update flow widget if initialized
|
||||
if self._flow_widget:
|
||||
self._flow_widget.set_tool(self._tool)
|
||||
|
||||
def _add_argument(self):
|
||||
"""Add a new argument."""
|
||||
from ..dialogs.argument_dialog import ArgumentDialog
|
||||
@@ -315,14 +410,17 @@ class ToolBuilderPage(QWidget):
|
||||
self._refresh_steps()
|
||||
|
||||
def _edit_step(self):
|
||||
"""Edit selected step."""
|
||||
"""Edit selected step from list view."""
|
||||
items = self.steps_list.selectedItems()
|
||||
if not items:
|
||||
return
|
||||
|
||||
step = items[0].data(Qt.UserRole)
|
||||
idx = self.steps_list.row(items[0])
|
||||
self._edit_step_at_index(idx, step)
|
||||
|
||||
def _edit_step_at_index(self, idx: int, step):
|
||||
"""Edit a step at a specific index."""
|
||||
if isinstance(step, PromptStep):
|
||||
from ..dialogs.step_dialog import PromptStepDialog
|
||||
dialog = PromptStepDialog(self, step)
|
||||
|
||||
Reference in New Issue
Block a user