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:
rob 2026-01-15 22:36:13 -04:00
parent 4d9a0e5943
commit fb879d09ea
2 changed files with 105 additions and 58 deletions

View File

@ -307,13 +307,19 @@ class ToolBuilderPage(QWidget):
"""Refresh steps list and flow view."""
self.steps_list.clear()
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):
if isinstance(step, PromptStep):
# Use custom name if set, otherwise default format
step_name = step.name if step.name else f"Prompt {i}"
prompt_count += 1
# 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}"
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}"
else:
text = f"{i}. Unknown step"

View File

@ -2,9 +2,9 @@
from typing import Optional, List
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
from PySide6.QtCore import Signal, Qt, QTimer, QEvent
from PySide6.QtGui import QKeyEvent
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QMenu
from PySide6.QtCore import Signal, Qt, QTimer, QEvent, QPropertyAnimation, QEasingCurve
from PySide6.QtGui import QKeyEvent, QAction
from NodeGraphQt import NodeGraph, BaseNode
@ -138,52 +138,6 @@ class FlowGraphWidget(QWidget):
layout.setContentsMargins(0, 0, 0, 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
self._graph = NodeGraph()
@ -199,9 +153,49 @@ class FlowGraphWidget(QWidget):
# Add graph widget
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)
# 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]):
"""Set the tool to visualize."""
self._tool = tool
@ -234,10 +228,15 @@ class FlowGraphWidget(QWidget):
x_pos = 0
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 []):
if isinstance(step, PromptStep):
# Use custom name if set, otherwise default to "Prompt N"
node_name = step.name if step.name else f'Prompt {i+1}'
prompt_count += 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(
'cmdforge.PromptNode',
name=node_name,
@ -245,8 +244,9 @@ class FlowGraphWidget(QWidget):
)
node.set_step(step, i)
elif isinstance(step, CodeStep):
# Use custom name if set, otherwise default to "Code N"
node_name = step.name if step.name else f'Code {i+1}'
code_count += 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(
'cmdforge.CodeNode',
name=node_name,
@ -318,12 +318,53 @@ class FlowGraphWidget(QWidget):
super().keyPressEvent(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:
self.fit_all_nodes()
return True # Event handled
elif event.type() == QEvent.FocusIn:
self._show_help_banner()
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):
"""Handle node double-click."""
if hasattr(node, '_step_index') and node._step_index >= 0: