import { useEffect, useRef, useState } from 'react'; import { Handle, Position } from '@xyflow/react'; import { useWorkflow } from '../context/WorkflowContext.jsx'; function getStatusLabel(status) { switch (status) { case 'running': return 'Running'; case 'waiting': return 'Waiting'; case 'skipped': return 'Skipped'; case 'success': return 'Ready'; case 'error': return 'Error'; default: return ''; } } const HANDLE_COLORS_BY_ID = { answer: '#38bdf8', question: '#a78bfa', turn: '#f59e0b', dialog: '#22c55e', 'dialog-in': '#22c55e', }; const HANDLE_COLORS_BY_DATA_TYPE = { string: '#38bdf8', object: '#f59e0b', '*': '#94a3b8', }; function getHandleColor(handle) { return HANDLE_COLORS_BY_ID[handle?.id] || HANDLE_COLORS_BY_DATA_TYPE[handle?.dataType] || '#94a3b8'; } export default function NodeShell({ nodeId, title, accent, selected, status, inputs = [], outputs = [], children, footer, className = '', bodyClassName = '', renderInputAddon, }) { const { patchNodeData } = useWorkflow(); const rows = Math.max(inputs.length, outputs.length); const [isEditingTitle, setIsEditingTitle] = useState(false); const [draftTitle, setDraftTitle] = useState(title || ''); const titleInputRef = useRef(null); useEffect(() => { if (!isEditingTitle) { setDraftTitle(title || ''); } }, [isEditingTitle, title]); useEffect(() => { if (isEditingTitle && titleInputRef.current) { titleInputRef.current.focus(); titleInputRef.current.select(); } }, [isEditingTitle]); const finishTitleEditing = () => { const nextTitle = draftTitle.trim() || title || 'Untitled'; if (nodeId && nextTitle !== title) { patchNodeData(nodeId, { title: nextTitle }); } setDraftTitle(nextTitle); setIsEditingTitle(false); }; return (
{isEditingTitle ? ( setDraftTitle(event.target.value)} onBlur={finishTitleEditing} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); finishTitleEditing(); } if (event.key === 'Escape') { event.preventDefault(); setDraftTitle(title || ''); setIsEditingTitle(false); } }} /> ) : ( )} {status && status !== 'idle' ? (
{getStatusLabel(status)}
) : null}
{rows > 0 ? (
{Array.from({ length: rows }).map((_, index) => { const input = inputs[index]; const output = outputs[index]; return (
{input ? ( <>
{input.label} {renderInputAddon ? renderInputAddon(input, index) : null}
) : ( )}
{output ? ( <>
{output.label}
) : ( )}
); })}
) : null}
{children}
{footer ?
{footer}
: null}
); }