Improve flow view UX and fix step naming
- Remove Fit All button, F key shortcut is sufficient - Add right-click context menu with "Fit All (F)" option - Add floating help banner that appears on focus and auto-hides - Banner stays visible when mouse hovers over it - Fix default step naming to increment per type (Code 1, Prompt 1, Code 2) instead of globally (Code 1, Prompt 2, Code 3) - List view still shows overall step number (1, 2, 3) with per-type names Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d9a0e5943
commit
fb879d09ea
|
|
@ -307,13 +307,19 @@ class ToolBuilderPage(QWidget):
|
||||||
"""Refresh steps list and flow view."""
|
"""Refresh steps list and flow view."""
|
||||||
self.steps_list.clear()
|
self.steps_list.clear()
|
||||||
if self._tool and self._tool.steps:
|
if self._tool and self._tool.steps:
|
||||||
|
# Count steps by type for default naming
|
||||||
|
prompt_count = 0
|
||||||
|
code_count = 0
|
||||||
|
|
||||||
for i, step in enumerate(self._tool.steps, 1):
|
for i, step in enumerate(self._tool.steps, 1):
|
||||||
if isinstance(step, PromptStep):
|
if isinstance(step, PromptStep):
|
||||||
# Use custom name if set, otherwise default format
|
prompt_count += 1
|
||||||
step_name = step.name if step.name else f"Prompt {i}"
|
# Use custom name if set, otherwise default per-type naming
|
||||||
|
step_name = step.name if step.name else f"Prompt {prompt_count}"
|
||||||
text = f"{i}. {step_name} [{step.provider}] → ${step.output_var}"
|
text = f"{i}. {step_name} [{step.provider}] → ${step.output_var}"
|
||||||
elif isinstance(step, CodeStep):
|
elif isinstance(step, CodeStep):
|
||||||
step_name = step.name if step.name else f"Code {i}"
|
code_count += 1
|
||||||
|
step_name = step.name if step.name else f"Code {code_count}"
|
||||||
text = f"{i}. {step_name} [python] → ${step.output_var}"
|
text = f"{i}. {step_name} [python] → ${step.output_var}"
|
||||||
else:
|
else:
|
||||||
text = f"{i}. Unknown step"
|
text = f"{i}. Unknown step"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QMenu
|
||||||
from PySide6.QtCore import Signal, Qt, QTimer, QEvent
|
from PySide6.QtCore import Signal, Qt, QTimer, QEvent, QPropertyAnimation, QEasingCurve
|
||||||
from PySide6.QtGui import QKeyEvent
|
from PySide6.QtGui import QKeyEvent, QAction
|
||||||
|
|
||||||
from NodeGraphQt import NodeGraph, BaseNode
|
from NodeGraphQt import NodeGraph, BaseNode
|
||||||
|
|
||||||
|
|
@ -138,52 +138,6 @@ class FlowGraphWidget(QWidget):
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(0)
|
layout.setSpacing(0)
|
||||||
|
|
||||||
# Navigation help bar
|
|
||||||
help_bar = QWidget()
|
|
||||||
help_bar.setStyleSheet("""
|
|
||||||
QWidget {
|
|
||||||
background-color: #edf2f7;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
QLabel {
|
|
||||||
color: #718096;
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
help_layout = QHBoxLayout(help_bar)
|
|
||||||
help_layout.setContentsMargins(8, 2, 8, 2)
|
|
||||||
|
|
||||||
help_text = QLabel(
|
|
||||||
"Pan: Middle-click drag | "
|
|
||||||
"Zoom: Scroll wheel | "
|
|
||||||
"Select: Click or drag box | "
|
|
||||||
"Edit step: Double-click node"
|
|
||||||
)
|
|
||||||
help_layout.addWidget(help_text)
|
|
||||||
help_layout.addStretch()
|
|
||||||
|
|
||||||
# Fit view button
|
|
||||||
self.btn_fit = QPushButton("Fit All (F)")
|
|
||||||
self.btn_fit.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: #667eea;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #5a67d8;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.btn_fit.clicked.connect(self.fit_all_nodes)
|
|
||||||
help_layout.addWidget(self.btn_fit)
|
|
||||||
|
|
||||||
layout.addWidget(help_bar)
|
|
||||||
|
|
||||||
# Create node graph
|
# Create node graph
|
||||||
self._graph = NodeGraph()
|
self._graph = NodeGraph()
|
||||||
|
|
||||||
|
|
@ -199,9 +153,49 @@ class FlowGraphWidget(QWidget):
|
||||||
# Add graph widget
|
# Add graph widget
|
||||||
layout.addWidget(self._graph.widget, 1)
|
layout.addWidget(self._graph.widget, 1)
|
||||||
|
|
||||||
# Install event filter to catch F key on the graph widget
|
# Install event filter to catch F key and focus events on the graph widget
|
||||||
self._graph.widget.installEventFilter(self)
|
self._graph.widget.installEventFilter(self)
|
||||||
|
|
||||||
|
# Create floating help banner (overlay on graph widget)
|
||||||
|
self._help_banner = QLabel(self._graph.widget)
|
||||||
|
self._help_banner.setText(
|
||||||
|
"Pan: Middle-click drag | "
|
||||||
|
"Zoom: Scroll wheel | "
|
||||||
|
"Select: Click or drag box | "
|
||||||
|
"Edit: Double-click | "
|
||||||
|
"Fit All: F"
|
||||||
|
)
|
||||||
|
self._help_banner.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
background-color: rgba(45, 55, 72, 0.9);
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self._help_banner.adjustSize()
|
||||||
|
self._help_banner.hide()
|
||||||
|
|
||||||
|
# Animation for fading
|
||||||
|
self._fade_animation = QPropertyAnimation(self._help_banner, b"windowOpacity")
|
||||||
|
self._fade_animation.setDuration(500)
|
||||||
|
self._fade_animation.setEasingCurve(QEasingCurve.InOutQuad)
|
||||||
|
|
||||||
|
# Timer for auto-hide
|
||||||
|
self._hide_timer = QTimer(self)
|
||||||
|
self._hide_timer.setSingleShot(True)
|
||||||
|
self._hide_timer.timeout.connect(self._fade_out_banner)
|
||||||
|
|
||||||
|
# Track if mouse is over banner
|
||||||
|
self._help_banner.setMouseTracking(True)
|
||||||
|
self._help_banner.enterEvent = self._on_banner_enter
|
||||||
|
self._help_banner.leaveEvent = self._on_banner_leave
|
||||||
|
|
||||||
|
# Set up context menu
|
||||||
|
self._graph.widget.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
|
self._graph.widget.customContextMenuRequested.connect(self._show_context_menu)
|
||||||
|
|
||||||
def set_tool(self, tool: Optional[Tool]):
|
def set_tool(self, tool: Optional[Tool]):
|
||||||
"""Set the tool to visualize."""
|
"""Set the tool to visualize."""
|
||||||
self._tool = tool
|
self._tool = tool
|
||||||
|
|
@ -234,10 +228,15 @@ class FlowGraphWidget(QWidget):
|
||||||
x_pos = 0
|
x_pos = 0
|
||||||
prev_node = self._input_node
|
prev_node = self._input_node
|
||||||
|
|
||||||
|
# Count steps by type for default naming
|
||||||
|
prompt_count = 0
|
||||||
|
code_count = 0
|
||||||
|
|
||||||
for i, step in enumerate(self._tool.steps or []):
|
for i, step in enumerate(self._tool.steps or []):
|
||||||
if isinstance(step, PromptStep):
|
if isinstance(step, PromptStep):
|
||||||
# Use custom name if set, otherwise default to "Prompt N"
|
prompt_count += 1
|
||||||
node_name = step.name if step.name else f'Prompt {i+1}'
|
# Use custom name if set, otherwise default to "Prompt N" (per type)
|
||||||
|
node_name = step.name if step.name else f'Prompt {prompt_count}'
|
||||||
node = self._graph.create_node(
|
node = self._graph.create_node(
|
||||||
'cmdforge.PromptNode',
|
'cmdforge.PromptNode',
|
||||||
name=node_name,
|
name=node_name,
|
||||||
|
|
@ -245,8 +244,9 @@ class FlowGraphWidget(QWidget):
|
||||||
)
|
)
|
||||||
node.set_step(step, i)
|
node.set_step(step, i)
|
||||||
elif isinstance(step, CodeStep):
|
elif isinstance(step, CodeStep):
|
||||||
# Use custom name if set, otherwise default to "Code N"
|
code_count += 1
|
||||||
node_name = step.name if step.name else f'Code {i+1}'
|
# Use custom name if set, otherwise default to "Code N" (per type)
|
||||||
|
node_name = step.name if step.name else f'Code {code_count}'
|
||||||
node = self._graph.create_node(
|
node = self._graph.create_node(
|
||||||
'cmdforge.CodeNode',
|
'cmdforge.CodeNode',
|
||||||
name=node_name,
|
name=node_name,
|
||||||
|
|
@ -318,12 +318,53 @@ class FlowGraphWidget(QWidget):
|
||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
"""Filter events from the graph widget to catch F key."""
|
"""Filter events from the graph widget to catch F key and focus."""
|
||||||
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_F:
|
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_F:
|
||||||
self.fit_all_nodes()
|
self.fit_all_nodes()
|
||||||
return True # Event handled
|
return True # Event handled
|
||||||
|
elif event.type() == QEvent.FocusIn:
|
||||||
|
self._show_help_banner()
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
def _show_help_banner(self):
|
||||||
|
"""Show the help banner with fade effect."""
|
||||||
|
if not self._help_banner:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Position at top center of the graph widget
|
||||||
|
parent_width = self._graph.widget.width()
|
||||||
|
banner_width = self._help_banner.width()
|
||||||
|
x = (parent_width - banner_width) // 2
|
||||||
|
self._help_banner.move(x, 10)
|
||||||
|
|
||||||
|
# Show and start auto-hide timer
|
||||||
|
self._help_banner.show()
|
||||||
|
self._hide_timer.start(3000) # Hide after 3 seconds
|
||||||
|
|
||||||
|
def _fade_out_banner(self):
|
||||||
|
"""Fade out the help banner."""
|
||||||
|
if self._help_banner and self._help_banner.isVisible():
|
||||||
|
# Use a simple hide with timer since windowOpacity doesn't work well on child widgets
|
||||||
|
self._help_banner.hide()
|
||||||
|
|
||||||
|
def _on_banner_enter(self, event):
|
||||||
|
"""Mouse entered banner - stop hide timer."""
|
||||||
|
self._hide_timer.stop()
|
||||||
|
|
||||||
|
def _on_banner_leave(self, event):
|
||||||
|
"""Mouse left banner - restart hide timer."""
|
||||||
|
self._hide_timer.start(1500) # Hide 1.5 seconds after mouse leaves
|
||||||
|
|
||||||
|
def _show_context_menu(self, pos):
|
||||||
|
"""Show context menu."""
|
||||||
|
menu = QMenu(self._graph.widget)
|
||||||
|
|
||||||
|
fit_action = QAction("Fit All (F)", menu)
|
||||||
|
fit_action.triggered.connect(self.fit_all_nodes)
|
||||||
|
menu.addAction(fit_action)
|
||||||
|
|
||||||
|
menu.exec_(self._graph.widget.mapToGlobal(pos))
|
||||||
|
|
||||||
def _on_node_double_clicked(self, node):
|
def _on_node_double_clicked(self, node):
|
||||||
"""Handle node double-click."""
|
"""Handle node double-click."""
|
||||||
if hasattr(node, '_step_index') and node._step_index >= 0:
|
if hasattr(node, '_step_index') and node._step_index >= 0:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue