diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 0be5079e0..3c1c32a41 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -207,6 +207,7 @@ function TemplateCardInner({ isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index b32db3ff9..5bb5886d1 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -214,6 +214,7 @@ function TemplateCardInner({ defaultZoom={0.8} fitPadding={0.2} cursorStyle='pointer' + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx index 415c830f0..6ff75f6f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx @@ -446,6 +446,7 @@ const OGCaptureContainer = forwardRef((_, ref) => { isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight />
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 762ad1f2d..cc124bdd6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -466,6 +466,7 @@ export function Editor() { defaultZoom={0.6} fitPadding={0.15} cursorStyle='grab' + lightweight />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx index a56e881de..84f53b415 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx @@ -1,6 +1,7 @@ 'use client' import { memo, useMemo } from 'react' +import { RepeatIcon, SplitIcon } from 'lucide-react' import { Handle, type NodeProps, Position } from 'reactflow' import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' import { @@ -10,14 +11,12 @@ import { isSubBlockVisibleForMode, } from '@/lib/workflows/subblocks/visibility' import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' +import type { ExecutionStatus } from '@/app/workspace/[workspaceId]/w/components/preview/preview' import { getBlock } from '@/blocks' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { useVariablesStore } from '@/stores/panel/variables/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -/** Execution status for blocks in preview mode */ -type ExecutionStatus = 'success' | 'error' | 'not-executed' - /** Subblock value structure matching workflow state */ interface SubBlockValueEntry { value: unknown @@ -35,6 +34,16 @@ interface WorkflowPreviewBlockData { executionStatus?: ExecutionStatus /** Subblock values from the workflow state */ subBlockValues?: Record + /** Skips expensive subblock computations for thumbnails */ + lightweight?: boolean + /** Whether this is a subflow container (loop/parallel) */ + isSubflow?: boolean + /** Type of subflow container */ + subflowKind?: 'loop' | 'parallel' + /** Width of subflow container */ + width?: number + /** Height of subflow container */ + height?: number } /** @@ -204,21 +213,16 @@ function resolveToolsDisplay( * - Shows '-' for other selector types that need hydration */ function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { - // Mask password fields const isPasswordField = subBlock?.password === true const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null - // Resolve various display names (synchronous access, matching WorkflowBlock priority) const dropdownLabel = resolveDropdownLabel(subBlock, rawValue) const variablesDisplay = resolveVariablesDisplay(subBlock, rawValue) const toolsDisplay = resolveToolsDisplay(subBlock, rawValue) const workflowName = resolveWorkflowName(subBlock, rawValue) - // Check if this is a selector type that needs hydration (show '-' for raw IDs) const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type) - // Compute final display value matching WorkflowBlock logic - // Priority order matches WorkflowBlock: masked > hydrated names > selector fallback > raw value const hydratedName = dropdownLabel || variablesDisplay || toolsDisplay || workflowName const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) @@ -242,11 +246,115 @@ function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { ) } +interface SubflowContainerProps { + name: string + width?: number + height?: number + kind: 'loop' | 'parallel' + isPreviewSelected?: boolean +} + +/** + * Renders a subflow container (loop/parallel) for preview mode. + */ +function SubflowContainer({ + name, + width = 500, + height = 300, + kind, + isPreviewSelected = false, +}: SubflowContainerProps) { + const isLoop = kind === 'loop' + const BlockIcon = isLoop ? RepeatIcon : SplitIcon + const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B' + const blockName = name || (isLoop ? 'Loop' : 'Parallel') + + const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source' + const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source' + + const leftHandleClass = + '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none' + const rightHandleClass = + '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' + + return ( +
+ {/* Selection ring overlay */} + {isPreviewSelected && ( +
+ )} + + {/* Target handle on left (input to the subflow) */} + + + {/* Header - matches actual subflow header structure */} +
+
+
+ +
+ + {blockName} + +
+
+ + {/* Content area - matches workflow structure */} +
+ {/* Subflow Start - connects to first block in subflow */} +
+ Start + +
+
+ + {/* End source handle on right (output from the subflow) */} + +
+ ) +} + /** * Preview block component for workflow visualization. * Renders block header, subblock values, and handles without * hooks, store subscriptions, or interactive features. * Matches the visual structure of WorkflowBlock exactly. + * Also handles subflow containers (loop/parallel) when isSubflow is true. */ function WorkflowPreviewBlockInner({ data }: NodeProps) { const { @@ -258,53 +366,84 @@ function WorkflowPreviewBlockInner({ data }: NodeProps isPreviewSelected = false, executionStatus, subBlockValues, + lightweight = false, + isSubflow = false, + subflowKind, + width, + height, } = data + if (isSubflow && subflowKind) { + return ( + + ) + } + const blockConfig = getBlock(type) const canonicalIndex = useMemo( - () => buildCanonicalIndex(blockConfig?.subBlocks || []), - [blockConfig?.subBlocks] + () => + lightweight + ? { groupsById: {}, canonicalIdBySubBlockId: {} } + : buildCanonicalIndex(blockConfig?.subBlocks || []), + [blockConfig?.subBlocks, lightweight] ) const rawValues = useMemo(() => { - if (!subBlockValues) return {} + if (lightweight || !subBlockValues) return {} return Object.entries(subBlockValues).reduce>((acc, [key, entry]) => { acc[key] = extractValue(entry) return acc }, {}) - }, [subBlockValues]) + }, [subBlockValues, lightweight]) const visibleSubBlocks = useMemo(() => { - if (!blockConfig?.subBlocks) return [] + if (lightweight || !blockConfig?.subBlocks) return [] - const isStarterOrTrigger = - blockConfig.category === 'triggers' || type === 'starter' || isTrigger + const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers' + + const effectiveTrigger = isTrigger || type === 'starter' return blockConfig.subBlocks.filter((subBlock) => { if (subBlock.hidden) return false if (subBlock.hideFromPreview) return false if (!isSubBlockFeatureEnabled(subBlock)) return false - // Handle trigger mode visibility - if (subBlock.mode === 'trigger' && !isStarterOrTrigger) return false + if (effectiveTrigger) { + const isValidTriggerSubblock = isPureTriggerBlock + ? subBlock.mode === 'trigger' || !subBlock.mode + : subBlock.mode === 'trigger' + if (!isValidTriggerSubblock) return false + } else { + if (subBlock.mode === 'trigger') return false + } - // Check advanced mode visibility if (!isSubBlockVisibleForMode(subBlock, false, canonicalIndex, rawValues, undefined)) { return false } - // Check condition visibility if (!subBlock.condition) return true return evaluateSubBlockCondition(subBlock.condition, rawValues) }) - }, [blockConfig?.subBlocks, blockConfig?.category, type, isTrigger, canonicalIndex, rawValues]) + }, [ + lightweight, + blockConfig?.subBlocks, + blockConfig?.category, + blockConfig?.triggers?.enabled, + type, + isTrigger, + canonicalIndex, + rawValues, + ]) - /** - * Compute condition rows for condition blocks - */ const conditionRows = useMemo(() => { - if (type !== 'condition') return [] + if (lightweight || type !== 'condition') return [] const conditionsValue = rawValues.conditions const raw = typeof conditionsValue === 'string' ? conditionsValue : undefined @@ -332,13 +471,10 @@ function WorkflowPreviewBlockInner({ data }: NodeProps { id: 'if', title: 'if', value: '' }, { id: 'else', title: 'else', value: '' }, ] - }, [type, rawValues]) + }, [lightweight, type, rawValues]) - /** - * Compute router rows for router_v2 blocks - */ const routerRows = useMemo(() => { - if (type !== 'router_v2') return [] + if (lightweight || type !== 'router_v2') return [] const routesValue = rawValues.routes const raw = typeof routesValue === 'string' ? routesValue : undefined @@ -361,7 +497,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps } return [{ id: 'route1', value: '' }] - }, [type, rawValues]) + }, [lightweight, type, rawValues]) if (!blockConfig) { return null @@ -439,12 +575,10 @@ function WorkflowPreviewBlockInner({ data }: NodeProps {hasContentBelowHeader && (
{type === 'condition' ? ( - // Condition block: render condition rows conditionRows.map((cond) => ( )) ) : type === 'router_v2' ? ( - // Router block: render context + route rows <> ))} ) : ( - // Standard blocks: render visible subblocks visibleSubBlocks.map((subBlock) => { const rawValue = rawValues[subBlock.id] return ( @@ -479,18 +612,102 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
)} - {/* Source handle */} - + {/* Condition block handles - one per condition branch + error */} + {type === 'condition' && ( + <> + {conditionRows.map((cond, condIndex) => { + const topOffset = + HANDLE_POSITIONS.CONDITION_START_Y + condIndex * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT + return ( + + ) + })} + + + )} + + {/* Router block handles - one per route + error */} + {type === 'router_v2' && ( + <> + {routerRows.map((route, routeIndex) => { + // +1 row offset for context row at the top + const topOffset = + HANDLE_POSITIONS.CONDITION_START_Y + + (routeIndex + 1) * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT + return ( + + ) + })} + + + )} + + {/* Standard block handles - source + error (not for condition, router, or response) */} + {type !== 'condition' && type !== 'router_v2' && type !== 'response' && ( + <> + + {shouldShowDefaultHandles && ( + + )} + + )}
) } @@ -499,7 +716,6 @@ function shouldSkipPreviewBlockRender( prevProps: NodeProps, nextProps: NodeProps ): boolean { - // Check primitive props first (fast path) if ( prevProps.id !== nextProps.id || prevProps.data.type !== nextProps.data.type || @@ -508,12 +724,16 @@ function shouldSkipPreviewBlockRender( prevProps.data.horizontalHandles !== nextProps.data.horizontalHandles || prevProps.data.enabled !== nextProps.data.enabled || prevProps.data.isPreviewSelected !== nextProps.data.isPreviewSelected || - prevProps.data.executionStatus !== nextProps.data.executionStatus + prevProps.data.executionStatus !== nextProps.data.executionStatus || + prevProps.data.lightweight !== nextProps.data.lightweight || + prevProps.data.isSubflow !== nextProps.data.isSubflow || + prevProps.data.subflowKind !== nextProps.data.subflowKind || + prevProps.data.width !== nextProps.data.width || + prevProps.data.height !== nextProps.data.height ) { return false } - // Compare subBlockValues by reference first const prevValues = prevProps.data.subBlockValues const nextValues = nextProps.data.subBlockValues @@ -525,7 +745,6 @@ function shouldSkipPreviewBlockRender( return false } - // Shallow compare keys and values const prevKeys = Object.keys(prevValues) const nextKeys = Object.keys(nextValues) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx deleted file mode 100644 index a9aa913a2..000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client' - -import { memo } from 'react' -import { RepeatIcon, SplitIcon } from 'lucide-react' -import { Handle, type NodeProps, Position } from 'reactflow' -import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' - -interface WorkflowPreviewSubflowData { - name: string - width?: number - height?: number - kind: 'loop' | 'parallel' - /** Whether this subflow is selected in preview mode */ - isPreviewSelected?: boolean -} - -/** - * Preview subflow component for workflow visualization. - * Renders loop/parallel containers without hooks, store subscriptions, - * or interactive features. - */ -function WorkflowPreviewSubflowInner({ data }: NodeProps) { - const { name, width = 500, height = 300, kind, isPreviewSelected = false } = data - - const isLoop = kind === 'loop' - const BlockIcon = isLoop ? RepeatIcon : SplitIcon - const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B' - const blockName = name || (isLoop ? 'Loop' : 'Parallel') - - const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source' - const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source' - - const leftHandleClass = - '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none' - const rightHandleClass = - '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' - - return ( -
- {/* Selection ring overlay */} - {isPreviewSelected && ( -
- )} - - {/* Target handle on left (input to the subflow) */} - - - {/* Header - matches actual subflow header structure */} -
-
-
- -
- - {blockName} - -
-
- - {/* Content area - matches workflow structure */} -
- {/* Subflow Start - connects to first block in subflow */} -
- Start - -
-
- - {/* End source handle on right (output from the subflow) */} - -
- ) -} - -export const WorkflowPreviewSubflow = memo(WorkflowPreviewSubflowInner) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index a9d56c794..ec00e2c38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -18,7 +18,6 @@ import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/b import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/block' -import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/subflow' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowPreview') @@ -53,13 +52,12 @@ function getPreviewBlockDimensions(block: BlockState): { width: number; height: /** * Calculates container dimensions based on child block positions and sizes. * Mirrors the logic from useNodeUtilities.calculateLoopDimensions. + * Accepts pre-filtered childBlocks for O(1) lookup instead of filtering all blocks. */ -function calculateContainerDimensions( - containerId: string, - blocks: Record -): { width: number; height: number } { - const childBlocks = Object.values(blocks).filter((block) => block?.data?.parentId === containerId) - +function calculateContainerDimensions(childBlocks: BlockState[]): { + width: number + height: number +} { if (childBlocks.length === 0) { return { width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, @@ -113,8 +111,34 @@ export function getLeftmostBlockId(workflowState: WorkflowState | null | undefin return leftmostId } +/** + * Recursively calculates the absolute position of a block, + * accounting for parent container offsets. + */ +function calculateAbsolutePosition( + block: BlockState, + blocks: Record +): { x: number; y: number } { + if (!block.data?.parentId) { + return block.position + } + + const parentBlock = blocks[block.data.parentId] + if (!parentBlock) { + logger.warn(`Parent block not found for child block: ${block.id}`) + return block.position + } + + const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks) + + return { + x: parentAbsolutePosition.x + block.position.x, + y: parentAbsolutePosition.y + block.position.y, + } +} + /** Execution status for edges/nodes in the preview */ -type ExecutionStatus = 'success' | 'error' | 'not-executed' +export type ExecutionStatus = 'success' | 'error' | 'not-executed' interface WorkflowPreviewProps { workflowState: WorkflowState @@ -136,6 +160,8 @@ interface WorkflowPreviewProps { executedBlocks?: Record /** Currently selected block ID for highlighting */ selectedBlockId?: string | null + /** Skips expensive computations for thumbnails/template previews */ + lightweight?: boolean } /** @@ -144,8 +170,6 @@ interface WorkflowPreviewProps { */ const previewNodeTypes: NodeTypes = { workflowBlock: WorkflowPreviewBlock, - noteBlock: WorkflowPreviewBlock, - subflowNode: WorkflowPreviewSubflow, } // Define edge types @@ -221,64 +245,30 @@ export function WorkflowPreview({ cursorStyle = 'grab', executedBlocks, selectedBlockId, + lightweight = false, }: WorkflowPreviewProps) { const containerRef = useRef(null) - const nodeTypes = previewNodeTypes const isValidWorkflowState = workflowState?.blocks && workflowState.edges - const blocksStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.blocks || {}).length, - ids: Object.keys(workflowState.blocks || {}).join(','), - } - }, [workflowState.blocks, isValidWorkflowState]) + const workflowStructureIds = useMemo(() => { + if (!isValidWorkflowState) return '' + const blockIds = Object.keys(workflowState.blocks || {}) + const edgeIds = (workflowState.edges || []).map((e) => e.id) + return [...blockIds, ...edgeIds].join(',') + }, [workflowState.blocks, workflowState.edges, isValidWorkflowState]) - const loopsStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.loops || {}).length, - ids: Object.keys(workflowState.loops || {}).join(','), + // Pre-compute parent-child relationships for O(1) lookups in container dimension calculations + const containerChildIndex = useMemo(() => { + const index: Record = {} + for (const block of Object.values(workflowState.blocks || {})) { + if (block?.data?.parentId) { + const parentId = block.data.parentId + if (!index[parentId]) index[parentId] = [] + index[parentId].push(block) + } } - }, [workflowState.loops, isValidWorkflowState]) - - const parallelsStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.parallels || {}).length, - ids: Object.keys(workflowState.parallels || {}).join(','), - } - }, [workflowState.parallels, isValidWorkflowState]) - - const edgesStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: workflowState.edges?.length || 0, - ids: workflowState.edges?.map((e) => e.id).join(',') || '', - } - }, [workflowState.edges, isValidWorkflowState]) - - const calculateAbsolutePosition = ( - block: any, - blocks: Record - ): { x: number; y: number } => { - if (!block.data?.parentId) { - return block.position - } - - const parentBlock = blocks[block.data.parentId] - if (!parentBlock) { - logger.warn(`Parent block not found for child block: ${block.id}`) - return block.position - } - - const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks) - - return { - x: parentAbsolutePosition.x + block.position.x, - y: parentAbsolutePosition.y + block.position.y, - } - } + return index + }, [workflowState.blocks]) const nodes: Node[] = useMemo(() => { if (!isValidWorkflowState) return [] @@ -293,21 +283,24 @@ export function WorkflowPreview({ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks) - // Handle loop/parallel containers + // Handle loop/parallel containers - use unified workflowBlock with isSubflow flag if (block.type === 'loop' || block.type === 'parallel') { const isSelected = selectedBlockId === blockId - const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) + const childBlocks = containerChildIndex[blockId] || [] + const dimensions = calculateContainerDimensions(childBlocks) nodeArray.push({ id: blockId, - type: 'subflowNode', + type: 'workflowBlock', position: absolutePosition, draggable: false, data: { + type: block.type, name: block.name, + isPreviewSelected: isSelected, + isSubflow: true, + subflowKind: block.type as 'loop' | 'parallel', width: dimensions.width, height: dimensions.height, - kind: block.type as 'loop' | 'parallel', - isPreviewSelected: isSelected, }, }) return @@ -348,19 +341,20 @@ export function WorkflowPreview({ isPreviewSelected: isSelected, executionStatus, subBlockValues: block.subBlocks, + lightweight, }, }) }) return nodeArray }, [ - blocksStructure, - loopsStructure, - parallelsStructure, + workflowStructureIds, workflowState.blocks, + containerChildIndex, isValidWorkflowState, executedBlocks, selectedBlockId, + lightweight, ]) const edges: Edge[] = useMemo(() => { @@ -395,7 +389,7 @@ export function WorkflowPreview({ zIndex: executionStatus === 'success' ? 10 : 0, } }) - }, [edgesStructure, workflowState.edges, isValidWorkflowState, executedBlocks]) + }, [workflowStructureIds, workflowState.edges, isValidWorkflowState, executedBlocks]) if (!isValidWorkflowState) { return ( @@ -449,7 +443,7 @@ export function WorkflowPreview({