@ -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 ,
} }
} }
/ >
/ >