/** * Extensible node type registry. Register node types with registerNodeType(); * built-in types are registered in registerBuiltinNodes.ts. * Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI. */ import type React from 'react' import type { Node } from '@xyflow/react' /** Node classification for UI: Psyche, Pneuma, Physis, Archon (AI Agent). */ export type NodeClassification = 'psyche' | 'pneuma' | 'physis' | 'archon' export const NODE_CLASSIFICATION_LABELS: Record = { psyche: 'Psyche', pneuma: 'Pneuma', physis: 'Physis', archon: 'Archon', } export type NodeHelpEntry = { title: string content: React.ReactNode } export type NodeTypeDescriptor = { id: string component: React.ComponentType defaultStyle: { width: number; height: number } defaultData: Record idPrefix: string hasInput: boolean hasOutput: boolean /** Classification for creation menu and node footer: Psyche, Pneuma, Physis. */ classification?: NodeClassification /** When this type is the connection target, which source types are allowed. Omit = all. */ allowedSourceTypes?: string[] /** When this type is the connection source, which target types are allowed. Omit = all. */ allowedTargetTypes?: string[] help: NodeHelpEntry menuLabel: string menuIcon: React.ReactNode /** Optional: override default data for new nodes (e.g. set title from newId). */ getDefaultData?: (newId?: string) => Record /** Optional: data for Reset action; if omitted, getDefaultData(nodeId) or defaultData is used. */ getResetData?: (nodeId?: string) => Record /** Optional: label shown on edge when this type is the target (e.g. "render", "add input"). */ connectionLabel?: string /** When true, double-clicking the node header opens a fullscreen dialog for this node. */ supportsFullscreen?: boolean } const registry = new Map() export function registerNodeType(descriptor: NodeTypeDescriptor): void { if (registry.has(descriptor.id)) { console.warn(`[nodeRegistry] Overwriting existing node type: ${descriptor.id}`) } registry.set(descriptor.id, descriptor) } export function getNodeType(id: string): NodeTypeDescriptor | undefined { return registry.get(id) } const CLASSIFICATION_ORDER: NodeClassification[] = ['psyche', 'pneuma', 'physis', 'archon'] export function getRegisteredNodeTypes(): NodeTypeDescriptor[] { return Array.from(registry.values()) } /** Node types grouped by classification for the Create Node menu. Order: Psyche, Pneuma, Physis. */ export function getRegisteredNodeTypesGroupedByClassification(): { classification: NodeClassification label: string types: NodeTypeDescriptor[] }[] { const byClass = new Map() for (const c of CLASSIFICATION_ORDER) byClass.set(c, []) for (const desc of registry.values()) { const c = desc.classification ?? 'physis' byClass.get(c)!.push(desc) } return CLASSIFICATION_ORDER.map((classification) => ({ classification, label: NODE_CLASSIFICATION_LABELS[classification], types: byClass.get(classification)!, })) } export function getRegisteredNodeTypeIds(): string[] { return Array.from(registry.keys()) } /** Default data for a new node of this type. Uses getDefaultData from descriptor if provided. */ export function getDefaultDataForType(type: string, newId?: string): Record { const desc = registry.get(type) if (!desc) return {} if (desc.getDefaultData) return { ...desc.getDefaultData(newId) } const base = { ...desc.defaultData } if (type === 'config' && newId) (base as any).title = `${newId}` return base } /** Data for Reset action. Uses getResetData from descriptor if provided. */ export function getResetDataForType(type: string, nodeId?: string): Record { const desc = registry.get(type) if (!desc) return getDefaultDataForType(type, nodeId) if (desc.getResetData) return { ...desc.getResetData(nodeId) } return getDefaultDataForType(type, nodeId) } /** Id prefix for this type (e.g. cfg_, rnd_). Used by getNextNodeId. */ export function getIdPrefix(type: string): string { return getNodeType(type)?.idPrefix ?? 'node_' } /** Default style for this type. */ export function getDefaultStyle(type: string): { width: number; height: number } { const desc = getNodeType(type) if (desc) return desc.defaultStyle return { width: 320, height: 320 } } /** Whether a connection from source to target is allowed based on registered types. */ export function isConnectionAllowed( sourceType: string, targetType: string, sourceNodeId: string, targetNodeId: string ): boolean { if (sourceNodeId === targetNodeId) return false const sourceDesc = getNodeType(sourceType) const targetDesc = getNodeType(targetType) if (!sourceDesc || !targetDesc) return false if (!targetDesc.hasInput) return false if (!sourceDesc.hasOutput) return false if (targetDesc.allowedSourceTypes != null && !targetDesc.allowedSourceTypes.includes(sourceType)) return false if (sourceDesc.allowedTargetTypes != null && !sourceDesc.allowedTargetTypes.includes(targetType)) return false return true } /** Edge label when target is of this type (for AnimatedEdge). */ export function getConnectionLabelForTarget(targetType: string): string | undefined { return getNodeType(targetType)?.connectionLabel } /** Help entry for a node type. Use in NodeHelpPopover. */ export function getNodeHelp(nodeType: string): NodeHelpEntry { const desc = getNodeType(nodeType) return desc?.help ?? { title: nodeType, content: null } } /** Classification label for a node type (e.g. "Psyche"). Shown in node footer before help button. */ export function getNodeClassificationLabel(nodeType: string): string | null { const desc = getNodeType(nodeType) const c = desc?.classification return c ? NODE_CLASSIFICATION_LABELS[c] : null }