From a93b41f95aa28938577e62ff931a114450568135 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Fri, 16 Jan 2026 22:55:17 +1300 Subject: [PATCH] refactor(26-01): move workflow preview to modal with icon trigger - Restore image thumbnails as main preview in TemplateCard - Add small icon button in top right to trigger workflow preview - Create WorkflowPreviewModal for larger preview display - Fix WorkflowPreview to properly show all nodes with fitView - Add legend showing node type colors in modal --- src/components/quickstart/TemplateCard.tsx | 49 +++++++- .../quickstart/TemplateExplorerView.tsx | 29 ++++- src/components/quickstart/WorkflowPreview.tsx | 50 +++++--- .../quickstart/WorkflowPreviewModal.tsx | 117 ++++++++++++++++++ 4 files changed, 223 insertions(+), 22 deletions(-) create mode 100644 src/components/quickstart/WorkflowPreviewModal.tsx diff --git a/src/components/quickstart/TemplateCard.tsx b/src/components/quickstart/TemplateCard.tsx index 97b51fa0..788d1b4d 100644 --- a/src/components/quickstart/TemplateCard.tsx +++ b/src/components/quickstart/TemplateCard.tsx @@ -2,7 +2,6 @@ import { Node, Edge } from "@xyflow/react"; import { TemplateCategory } from "@/types/quickstart"; -import { WorkflowPreview } from "./WorkflowPreview"; interface TemplateCardProps { template: { @@ -14,9 +13,11 @@ interface TemplateCardProps { tags: string[]; }; nodeCount: number; + previewImage?: string; workflow?: { nodes: Node[]; edges: Edge[] }; isLoading: boolean; onClick: () => void; + onPreviewClick?: () => void; disabled: boolean; } @@ -37,9 +38,11 @@ const CATEGORY_COLORS: Record = { export function TemplateCard({ template, nodeCount, + previewImage, workflow, isLoading, onClick, + onPreviewClick, disabled, }: TemplateCardProps) { return ( @@ -89,8 +92,13 @@ export function TemplateCard({ /> - ) : workflow ? ( - + ) : previewImage ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + {`${template.name} ) : (
)} + + {/* Workflow Preview Button - show in top right when workflow is available */} + {workflow && !isLoading && ( +
{ + e.stopPropagation(); + onPreviewClick?.(); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + onPreviewClick?.(); + } + }} + className="absolute top-2 right-2 p-1.5 rounded-md bg-neutral-900/80 hover:bg-neutral-800 border border-neutral-600 hover:border-neutral-500 transition-colors cursor-pointer" + title="Preview workflow" + > + {/* Node graph icon */} + + + +
+ )} {/* Name */} diff --git a/src/components/quickstart/TemplateExplorerView.tsx b/src/components/quickstart/TemplateExplorerView.tsx index e2e7d2ba..a56e3f1b 100644 --- a/src/components/quickstart/TemplateExplorerView.tsx +++ b/src/components/quickstart/TemplateExplorerView.tsx @@ -2,9 +2,10 @@ import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { WorkflowFile } from "@/store/workflowStore"; -import { getAllPresets, PRESET_TEMPLATES } from "@/lib/quickstart/templates"; +import { getAllPresets, PRESET_TEMPLATES, getTemplateContent } from "@/lib/quickstart/templates"; import { QuickstartBackButton } from "./QuickstartBackButton"; import { TemplateCard } from "./TemplateCard"; +import { WorkflowPreviewModal } from "./WorkflowPreviewModal"; import { CommunityWorkflowMeta, TemplateCategory, TemplateMetadata } from "@/types/quickstart"; interface TemplateExplorerViewProps { @@ -30,6 +31,7 @@ export function TemplateExplorerView({ const [isLoadingList, setIsLoadingList] = useState(true); const [loadingWorkflowId, setLoadingWorkflowId] = useState(null); const [error, setError] = useState(null); + const [previewTemplateId, setPreviewTemplateId] = useState(null); // Filter state const [searchQuery, setSearchQuery] = useState(""); @@ -81,6 +83,19 @@ export function TemplateExplorerView({ return workflows; }, []); + // Get preview image for each template (first image from "full" content level) + const previewImages = useMemo(() => { + const images: Record = {}; + PRESET_TEMPLATES.forEach((template) => { + const content = getTemplateContent(template.id, "full"); + if (content?.images) { + const imageUrls = Object.values(content.images).map((img) => img.url); + images[template.id] = imageUrls[0]; + } + }); + return images; + }, []); + // Filter presets based on search, category, and tags const filteredPresets = useMemo(() => { return presets.filter((preset) => { @@ -404,9 +419,11 @@ export function TemplateExplorerView({ key={preset.id} template={preset} nodeCount={presetMetadata[preset.id]?.nodeCount ?? 0} + previewImage={previewImages[preset.id]} workflow={presetWorkflows[preset.id]} isLoading={loadingWorkflowId === preset.id} onClick={() => handlePresetSelect(preset.id)} + onPreviewClick={() => setPreviewTemplateId(preset.id)} disabled={isLoading} /> ))} @@ -579,6 +596,16 @@ export function TemplateExplorerView({ )} + + {/* Workflow Preview Modal */} + {previewTemplateId && presetWorkflows[previewTemplateId] && ( + setPreviewTemplateId(null)} + templateName={presets.find((p) => p.id === previewTemplateId)?.name || "Template"} + workflow={presetWorkflows[previewTemplateId]} + /> + )} ); } diff --git a/src/components/quickstart/WorkflowPreview.tsx b/src/components/quickstart/WorkflowPreview.tsx index b2c4d8eb..be69903a 100644 --- a/src/components/quickstart/WorkflowPreview.tsx +++ b/src/components/quickstart/WorkflowPreview.tsx @@ -1,15 +1,17 @@ "use client"; -import { useMemo } from "react"; +import { useMemo, useCallback } from "react"; import { ReactFlow, ReactFlowProvider, Node, Edge, + useReactFlow, + NodeProps, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; -// Node type to color mapping - using minimap colors from WorkflowCanvas +// Node type to color mapping - using similar colors from WorkflowCanvas minimap const NODE_COLORS: Record = { imageInput: "#22c55e", // green-500 annotation: "#eab308", // yellow-500 @@ -22,8 +24,9 @@ const NODE_COLORS: Record = { }; // Simple preview node component - just a colored rectangle -function PreviewNode({ data }: { data: { nodeType: string } }) { - const color = NODE_COLORS[data.nodeType] || "#6b7280"; +function PreviewNode({ data }: NodeProps) { + const nodeType = data?.nodeType as string || "unknown"; + const color = NODE_COLORS[nodeType] || "#6b7280"; return (
); @@ -50,40 +54,51 @@ interface WorkflowPreviewProps { className?: string; } +// Inner component that can use useReactFlow function WorkflowPreviewInner({ workflow, className = "" }: WorkflowPreviewProps) { - // Transform workflow nodes to preview nodes + const { fitView } = useReactFlow(); + + // Transform workflow nodes to preview nodes - keep original sizes/positions for better layout const previewNodes = useMemo(() => { return workflow.nodes.map((node) => ({ id: node.id, type: "preview", position: node.position, data: { nodeType: node.type || "unknown" }, - // Scale down the node sizes for preview + // Use scaled-down versions of the original dimensions style: { - width: ((node.style?.width as number) || 300) * 0.15, - height: ((node.style?.height as number) || 280) * 0.15, + width: ((node.style?.width as number) || 300) * 0.3, + height: ((node.style?.height as number) || 280) * 0.3, }, })); }, [workflow.nodes]); - // Use the workflow edges as-is (simplified rendering) + // Use the workflow edges with simplified styling const previewEdges = useMemo(() => { return workflow.edges.map((edge) => ({ id: edge.id, source: edge.source, target: edge.target, - // Simple edge style - style: { stroke: "#525252", strokeWidth: 1 }, + style: { stroke: "#525252", strokeWidth: 2 }, type: "default", })); }, [workflow.edges]); + // Fit view when nodes load + const onInit = useCallback(() => { + // Small delay to ensure nodes are rendered + setTimeout(() => { + fitView({ padding: 0.3, duration: 0 }); + }, 50); + }, [fitView]); + return (
void; + templateName: string; + workflow: { nodes: Node[]; edges: Edge[] }; +} + +export function WorkflowPreviewModal({ + isOpen, + onClose, + templateName, + workflow, +}: WorkflowPreviewModalProps) { + // Handle escape key to close + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }, + [onClose] + ); + + useEffect(() => { + if (isOpen) { + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + } + }, [isOpen, handleKeyDown]); + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
e.stopPropagation()} + > + {/* Header */} +
+

+ {templateName} — Workflow Preview +

+ +
+ + {/* Preview Content */} +
+
+ +
+
+ + {/* Legend */} +
+
+ Node Types: +
+
+ Image Input +
+
+
+ Prompt +
+
+
+ Generate Image +
+
+
+ Generate Video +
+
+
+ LLM +
+
+
+ Output +
+
+
+
+
+ ); +}