'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { domAnimation, LazyMotion, m } from 'framer-motion' import { Maximize2, X } from 'lucide-react' import ReactFlow, { applyEdgeChanges, applyNodeChanges, type Edge, type EdgeProps, type EdgeTypes, getSmoothStepPath, type Node, type NodeTypes, type OnEdgesChange, type OnNodesChange, ReactFlowProvider, } from 'reactflow' import 'reactflow/dist/style.css' import { BLOCK_DISPLAY_WORKFLOWS } from '@/components/workflow-preview/block-display-workflows' import { BlockInspector } from '@/components/workflow-preview/block-inspector' import { DocsBlockNode } from '@/components/workflow-preview/docs-block-node' import { DocsContainerNode } from '@/components/workflow-preview/docs-container-node' import { EASE_OUT, type PreviewBlock, type PreviewWorkflow, toReactFlowElements, } from '@/components/workflow-preview/workflow-data' interface WorkflowPreviewProps { workflow: PreviewWorkflow /** Canvas height in px. Default 260. */ height?: number animate?: boolean /** Emphasize one block by id, dimming the rest. */ highlightBlock?: string /** Emphasize one edge by id, dimming the rest. */ highlightEdge?: string } /** Smooth-step edge, matching the app's connection styling. */ function PreviewEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data, }: EdgeProps) { const [edgePath] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, borderRadius: 8, offset: 30, }) if (data?.animate) { return ( ) } return ( ) } const NODE_TYPES: NodeTypes = { previewBlock: DocsBlockNode, previewContainer: DocsContainerNode, } const EDGE_TYPES: EdgeTypes = { previewEdge: PreviewEdge } const PRO_OPTIONS = { hideAttribution: true } const FIT_VIEW_OPTIONS = { padding: 0.25, maxZoom: 1 } as const const LIGHTBOX_FIT_VIEW_OPTIONS = { padding: 0.3, maxZoom: 1.4 } as const /** Field titles rendered as multiline text in the inspector. */ const TEXTAREA_TITLES = new Set(['Messages', 'Prompt', 'Code', 'Data', 'Body', 'Display']) /** Field titles rendered as dropdowns in the inspector. */ const SELECT_TITLES = new Set([ 'Model', 'Operation', 'Method', 'Unit', 'Event type', 'Validation', 'Account', 'Table', 'Knowledge Base', 'Language', 'Workflow', 'Format', ]) function inspectorFieldsFor(block: PreviewBlock) { // Show the block type's full field list (from the reference data) with this // block's example values overlaid, so the inspector reads like the editor's // panel — every field — not just the summary rows shown on the canvas node. // Apply the template only when the block authored rows that are actually a // subset of it; otherwise the type string is ambiguous (a `table` action vs // the table trigger, a `webhook` trigger vs the webhook action) and the wrong // template would win, so the block's own authored rows are the source of // truth. A block with no rows (e.g. a router defined only by its branches) // keeps its empty set rather than inheriting the template's invented defaults. const exampleByTitle = new Map(block.rows.map((row) => [row.title, row.value])) const template = BLOCK_DISPLAY_WORKFLOWS[block.type]?.blocks[0]?.rows const fullRows = template && block.rows.length > 0 && block.rows.every((row) => template.some((field) => field.title === row.title)) ? template : block.rows const rowFields = fullRows.map((row) => { const value = exampleByTitle.get(row.title) ?? row.value return { label: row.title, kind: TEXTAREA_TITLES.has(row.title) || value.length > 40 ? ('textarea' as const) : SELECT_TITLES.has(row.title) ? ('select' as const) : ('input' as const), value, } }) const branchFields = (block.branches ?? []).map((branch) => ({ label: branch.label, kind: 'code' as const, // Match the canvas + the editor's getDisplayValue: a blank value reads as '-'. value: branch.value || '-', })) return [...rowFields, ...branchFields] } function PreviewFlow({ workflow, animate = false, highlightBlock, highlightEdge, selectedBlock, interactive = false, onNodeClick, onPaneClick, }: WorkflowPreviewProps & { selectedBlock?: string interactive?: boolean onNodeClick?: (blockId: string) => void onPaneClick?: () => void }) { const { nodes: initialNodes, edges: initialEdges } = useMemo( () => toReactFlowElements(workflow, animate, { highlightBlock, highlightEdge, selectedBlock }), [workflow, animate, highlightBlock, highlightEdge, selectedBlock] ) const [nodes, setNodes] = useState(initialNodes) const [edges, setEdges] = useState(initialEdges) /** * Apply data changes (highlight/selection) without discarding positions the * viewer has dragged — only a different workflow should relayout the canvas. */ useEffect(() => { setNodes((prev) => { const positions = new Map(prev.map((node) => [node.id, node.position])) return initialNodes.map((node) => { const position = positions.get(node.id) return position ? { ...node, position } : node }) }) setEdges(initialEdges) }, [initialNodes, initialEdges]) const onNodesChange: OnNodesChange = useCallback( (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), [] ) const onEdgesChange: OnEdgesChange = useCallback( (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), [] ) return ( onNodeClick(node.id) : undefined} onPaneClick={onPaneClick} nodeTypes={NODE_TYPES} edgeTypes={EDGE_TYPES} defaultEdgeOptions={{ type: 'previewEdge' }} elementsSelectable={false} nodesDraggable nodesConnectable={false} zoomOnScroll={interactive} zoomOnDoubleClick={interactive} panOnScroll={false} zoomOnPinch panOnDrag preventScrolling={interactive} autoPanOnNodeDrag={false} proOptions={PRO_OPTIONS} minZoom={0.1} fitView fitViewOptions={interactive ? LIGHTBOX_FIT_VIEW_OPTIONS : FIT_VIEW_OPTIONS} className='h-full w-full' /> ) } /** * Read-only, app-styled workflow diagram for docs pages. Renders a * {@link PreviewWorkflow} with ReactFlow — draggable, non-editable, no app * runtime. Clicking a block (or the expand control) opens a full-screen * lightbox with zoom and pan, plus a read-only inspector panel showing the * selected block's full configuration — canvas rows truncate, the inspector * doesn't. * * @example * */ export function WorkflowPreview({ workflow, height = 340, animate = false, highlightBlock, highlightEdge, }: WorkflowPreviewProps) { const [expanded, setExpanded] = useState(false) const [selectedId, setSelectedId] = useState(null) useEffect(() => { if (!expanded) return const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setExpanded(false) } document.addEventListener('keydown', onKey) const previousOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' document.body.classList.add('wp-lightbox-open') return () => { document.removeEventListener('keydown', onKey) document.body.style.overflow = previousOverflow document.body.classList.remove('wp-lightbox-open') } }, [expanded]) const selectedBlock = selectedId ? (workflow.blocks.find((b) => b.id === selectedId) ?? null) : null const openWith = (blockId: string | null) => { setSelectedId(blockId) setExpanded(true) } return (
openWith(id)} onPaneClick={() => openWith(null)} />
{expanded && (
setExpanded(false)} onKeyDown={() => {}} role='presentation' >
e.stopPropagation()} onKeyDown={() => {}} role='presentation' >
{workflow.name}
setSelectedId(id)} onPaneClick={() => setSelectedId(null)} />
{selectedBlock ? ( ) : (
Select a block to see its full configuration
)}
)}
) }