Browse Source

feat(26-01): improve workflow preview with outlined nodes and labels

- Replace solid colored boxes with outlined nodes
- Add labels inside each node (Image Input, Prompt, etc.)
- Show model name for generate nodes (Gemini Pro, etc.)
- Use smoothstep edges for visible connections
- Simplify modal by removing redundant legend
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
d4b65f3933
  1. 159
      src/components/quickstart/WorkflowPreview.tsx
  2. 49
      src/components/quickstart/WorkflowPreviewModal.tsx

159
src/components/quickstart/WorkflowPreview.tsx

@ -8,10 +8,12 @@ import {
Edge, Edge,
useReactFlow, useReactFlow,
NodeProps, NodeProps,
Handle,
Position,
} from "@xyflow/react"; } from "@xyflow/react";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
// Node type to color mapping - using similar colors from WorkflowCanvas minimap // Node type to color mapping
const NODE_COLORS: Record<string, string> = { const NODE_COLORS: Record<string, string> = {
imageInput: "#22c55e", // green-500 imageInput: "#22c55e", // green-500
annotation: "#eab308", // yellow-500 annotation: "#eab308", // yellow-500
@ -23,22 +25,94 @@ const NODE_COLORS: Record<string, string> = {
output: "#6b7280", // gray-500 output: "#6b7280", // gray-500
}; };
// Simple preview node component - just a colored rectangle // Node type to display label mapping
const NODE_LABELS: Record<string, string> = {
imageInput: "Image Input",
annotation: "Annotation",
prompt: "Prompt",
nanoBanana: "Generate Image",
generateVideo: "Generate Video",
llmGenerate: "LLM",
splitGrid: "Split Grid",
output: "Output",
};
interface PreviewNodeData extends Record<string, unknown> {
nodeType: string;
label?: string;
model?: string;
}
// Preview node component with outline style and labels
function PreviewNode({ data }: NodeProps) { function PreviewNode({ data }: NodeProps) {
const nodeType = data?.nodeType as string || "unknown"; const nodeData = data as PreviewNodeData;
const nodeType = nodeData?.nodeType || "unknown";
const color = NODE_COLORS[nodeType] || "#6b7280"; const color = NODE_COLORS[nodeType] || "#6b7280";
const label = nodeData?.label || NODE_LABELS[nodeType] || nodeType;
const model = nodeData?.model;
// Determine if this is a generate node that should show model
const isGenerateNode = nodeType === "nanoBanana" || nodeType === "generateVideo";
return ( return (
<div <div
style={{ style={{
width: "100%", width: "100%",
height: "100%", height: "100%",
backgroundColor: color, backgroundColor: "rgba(23, 23, 23, 0.95)",
borderRadius: 6, border: `2px solid ${color}`,
border: "2px solid rgba(255,255,255,0.2)", borderRadius: 8,
boxShadow: "0 2px 4px rgba(0,0,0,0.3)", display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "8px 12px",
gap: 4,
}} }}
/> >
{/* Node label */}
<div
style={{
fontSize: 11,
fontWeight: 600,
color: color,
textAlign: "center",
lineHeight: 1.2,
}}
>
{label}
</div>
{/* Model name for generate nodes */}
{isGenerateNode && model && (
<div
style={{
fontSize: 9,
color: "rgba(255,255,255,0.6)",
textAlign: "center",
lineHeight: 1.2,
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{model}
</div>
)}
{/* Invisible handles for edge connections */}
<Handle
type="target"
position={Position.Left}
style={{ opacity: 0, pointerEvents: "none" }}
/>
<Handle
type="source"
position={Position.Right}
style={{ opacity: 0, pointerEvents: "none" }}
/>
</div>
); );
} }
@ -54,41 +128,72 @@ interface WorkflowPreviewProps {
className?: string; className?: string;
} }
// Helper to extract model name from node data
function getModelName(nodeData: Record<string, unknown>): string | undefined {
// Check for model field (common in generate nodes)
if (typeof nodeData?.model === "string") {
// Format model name to be more readable
const model = nodeData.model as string;
// Map internal names to display names
if (model === "nano-banana") return "Gemini Flash";
if (model === "nano-banana-pro") return "Gemini Pro";
// For other models, show shortened version
if (model.includes("/")) {
const parts = model.split("/");
return parts[parts.length - 1];
}
return model;
}
return undefined;
}
// Inner component that can use useReactFlow // Inner component that can use useReactFlow
function WorkflowPreviewInner({ workflow, className = "" }: WorkflowPreviewProps) { function WorkflowPreviewInner({ workflow, className = "" }: WorkflowPreviewProps) {
const { fitView } = useReactFlow(); const { fitView } = useReactFlow();
// Transform workflow nodes to preview nodes - keep original sizes/positions for better layout // Transform workflow nodes to preview nodes with labels
const previewNodes = useMemo(() => { const previewNodes = useMemo(() => {
return workflow.nodes.map((node) => ({ return workflow.nodes.map((node) => {
id: node.id, const nodeType = node.type || "unknown";
type: "preview", const nodeData = node.data as Record<string, unknown>;
position: node.position,
data: { nodeType: node.type || "unknown" }, return {
// Use scaled-down versions of the original dimensions id: node.id,
style: { type: "preview",
width: ((node.style?.width as number) || 300) * 0.3, position: node.position,
height: ((node.style?.height as number) || 280) * 0.3, data: {
}, nodeType,
})); label: NODE_LABELS[nodeType] || nodeType,
model: getModelName(nodeData),
} as PreviewNodeData,
// Use scaled-down versions of the original dimensions
style: {
width: ((node.style?.width as number) || 300) * 0.35,
height: ((node.style?.height as number) || 280) * 0.3,
},
};
});
}, [workflow.nodes]); }, [workflow.nodes]);
// Use the workflow edges with simplified styling // Create visible edges with better styling
const previewEdges = useMemo(() => { const previewEdges = useMemo(() => {
return workflow.edges.map((edge) => ({ return workflow.edges.map((edge) => ({
id: edge.id, id: edge.id,
source: edge.source, source: edge.source,
target: edge.target, target: edge.target,
style: { stroke: "#525252", strokeWidth: 2 }, type: "smoothstep",
type: "default", style: {
stroke: "#525252",
strokeWidth: 2,
},
animated: false,
})); }));
}, [workflow.edges]); }, [workflow.edges]);
// Fit view when nodes load // Fit view when nodes load
const onInit = useCallback(() => { const onInit = useCallback(() => {
// Small delay to ensure nodes are rendered
setTimeout(() => { setTimeout(() => {
fitView({ padding: 0.3, duration: 0 }); fitView({ padding: 0.2, duration: 0 });
}, 50); }, 50);
}, [fitView]); }, [fitView]);
@ -110,13 +215,13 @@ function WorkflowPreviewInner({ workflow, className = "" }: WorkflowPreviewProps
preventScrolling={true} preventScrolling={true}
// Fit the preview to container // Fit the preview to container
fitView={true} fitView={true}
fitViewOptions={{ padding: 0.3 }} fitViewOptions={{ padding: 0.2 }}
// Hide attribution // Hide attribution
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
// Styling // Styling
className="bg-transparent" className="bg-transparent"
defaultEdgeOptions={{ defaultEdgeOptions={{
type: "default", type: "smoothstep",
animated: false, animated: false,
}} }}
/> />

49
src/components/quickstart/WorkflowPreviewModal.tsx

@ -46,14 +46,19 @@ export function WorkflowPreviewModal({
{/* Modal */} {/* Modal */}
<div <div
className="relative bg-neutral-900 border border-neutral-700 rounded-xl shadow-2xl w-[90vw] max-w-3xl max-h-[85vh] flex flex-col" className="relative bg-neutral-900 border border-neutral-700 rounded-xl shadow-2xl w-[90vw] max-w-4xl max-h-[85vh] flex flex-col"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-neutral-700"> <div className="flex items-center justify-between px-5 py-4 border-b border-neutral-700">
<h3 className="text-base font-semibold text-neutral-100"> <div>
{templateName} Workflow Preview <h3 className="text-base font-semibold text-neutral-100">
</h3> {templateName}
</h3>
<p className="text-xs text-neutral-500 mt-0.5">
Workflow structure preview
</p>
</div>
<button <button
onClick={onClose} onClick={onClose}
className="p-1.5 rounded-md text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 transition-colors" className="p-1.5 rounded-md text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 transition-colors"
@ -76,40 +81,16 @@ export function WorkflowPreviewModal({
{/* Preview Content */} {/* Preview Content */}
<div className="flex-1 min-h-0 p-4"> <div className="flex-1 min-h-0 p-4">
<div className="w-full h-[60vh] bg-neutral-800/50 rounded-lg border border-neutral-700 overflow-hidden"> <div className="w-full h-[65vh] bg-neutral-950 rounded-lg border border-neutral-800 overflow-hidden">
<WorkflowPreview workflow={workflow} /> <WorkflowPreview workflow={workflow} />
</div> </div>
</div> </div>
{/* Legend */} {/* Footer hint */}
<div className="px-5 py-3 border-t border-neutral-700"> <div className="px-5 py-3 border-t border-neutral-700 text-center">
<div className="flex flex-wrap items-center gap-4 text-xs text-neutral-400"> <p className="text-xs text-neutral-500">
<span className="font-medium text-neutral-300">Node Types:</span> Press <kbd className="px-1.5 py-0.5 rounded bg-neutral-800 text-neutral-400 font-mono text-[10px]">Esc</kbd> or click outside to close
<div className="flex items-center gap-1.5"> </p>
<div className="w-3 h-3 rounded bg-[#22c55e]" />
<span>Image Input</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-[#3b82f6]" />
<span>Prompt</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-[#f97316]" />
<span>Generate Image</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-[#a855f7]" />
<span>Generate Video</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-[#06b6d4]" />
<span>LLM</span>
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded bg-[#6b7280]" />
<span>Output</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

Loading…
Cancel
Save