// ======================================== // Node Library Component // ======================================== // Displays built-in and custom node templates // Supports creating, saving, and deleting custom templates with color selection import { DragEvent, useState } from 'react'; import { useIntl } from 'react-intl'; import { MessageSquare, ChevronDown, ChevronRight, GripVertical, Terminal, Plus, Trash2, X, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useFlowStore } from '@/stores'; import { QUICK_TEMPLATES } from '@/types/flow'; import type { QuickTemplate } from '@/types/flow'; // ========== Icon Mapping ========== const TEMPLATE_ICONS: Record = { 'slash-command-main': Terminal, 'slash-command-async': Terminal, }; // ========== I18n Key Mapping for Built-in Templates ========== const TEMPLATE_I18N: Record = { 'prompt-template': { labelKey: 'orchestrator.nodeLibrary.promptTemplateLabel', descKey: 'orchestrator.nodeLibrary.promptTemplateDesc', }, 'slash-command-main': { labelKey: 'orchestrator.nodeLibrary.slashCommandLabel', descKey: 'orchestrator.nodeLibrary.slashCommandDesc', }, 'slash-command-async': { labelKey: 'orchestrator.nodeLibrary.slashCommandAsyncLabel', descKey: 'orchestrator.nodeLibrary.slashCommandAsyncDesc', }, }; // ========== Color Palette for custom templates ========== const COLOR_OPTIONS = [ { value: 'bg-blue-500', label: 'Blue' }, { value: 'bg-green-500', label: 'Green' }, { value: 'bg-purple-500', label: 'Purple' }, { value: 'bg-rose-500', label: 'Rose' }, { value: 'bg-amber-500', label: 'Amber' }, { value: 'bg-cyan-500', label: 'Cyan' }, { value: 'bg-teal-500', label: 'Teal' }, { value: 'bg-orange-500', label: 'Orange' }, { value: 'bg-indigo-500', label: 'Indigo' }, { value: 'bg-pink-500', label: 'Pink' }, ]; // ========== Sub-Components ========== /** * Collapsible category section with optional action button */ function TemplateCategory({ title, children, defaultExpanded = true, action, }: { title: string; children: React.ReactNode; defaultExpanded?: boolean; action?: React.ReactNode; }) { const [isExpanded, setIsExpanded] = useState(defaultExpanded); return (
{action}
{isExpanded &&
{children}
}
); } /** * Draggable card for a quick template */ function QuickTemplateCard({ template, onDelete, }: { template: QuickTemplate; onDelete?: () => void; }) { const { formatMessage } = useIntl(); const Icon = TEMPLATE_ICONS[template.id] || MessageSquare; const i18n = TEMPLATE_I18N[template.id]; const displayLabel = i18n ? formatMessage({ id: i18n.labelKey }) : template.label; const displayDesc = i18n ? formatMessage({ id: i18n.descKey }) : template.description; const onDragStart = (event: DragEvent) => { event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template'); event.dataTransfer.setData('application/reactflow-template-id', template.id); event.dataTransfer.effectAllowed = 'move'; }; const onDoubleClick = () => { useFlowStore.getState().addNodeFromTemplate(template.id, { x: 250, y: 200 }); }; return (
{displayLabel}
{displayDesc}
{onDelete ? ( ) : ( )}
); } /** * Basic empty prompt template card */ function BasicTemplateCard() { const { formatMessage } = useIntl(); const i18n = TEMPLATE_I18N['prompt-template']; const onDragStart = (event: DragEvent) => { event.dataTransfer.setData('application/reactflow-node-type', 'prompt-template'); event.dataTransfer.effectAllowed = 'move'; }; const onDoubleClick = () => { useFlowStore.getState().addNode({ x: 250, y: 200 }); }; return (
{formatMessage({ id: i18n.labelKey })}
{formatMessage({ id: i18n.descKey })}
); } /** * Inline form for creating a new custom template */ function CreateTemplateForm({ onClose }: { onClose: () => void }) { const { formatMessage } = useIntl(); const [label, setLabel] = useState(''); const [description, setDescription] = useState(''); const [instruction, setInstruction] = useState(''); const [color, setColor] = useState('bg-blue-500'); const addCustomTemplate = useFlowStore((s) => s.addCustomTemplate); const handleSubmit = () => { if (!label.trim()) return; const template: QuickTemplate = { id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, label: label.trim(), description: description.trim() || label.trim(), icon: 'MessageSquare', color, category: 'command', data: { label: label.trim(), instruction: instruction.trim(), contextRefs: [], }, }; addCustomTemplate(template); onClose(); }; return (
{formatMessage({ id: 'orchestrator.nodeLibrary.newCustomNode' })}
setLabel(e.target.value)} className="w-full text-sm px-2 py-1.5 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary" autoFocus /> setDescription(e.target.value)} className="w-full text-sm px-2 py-1.5 rounded border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary" />