Files
zui/frontend/src/lib/nodeRegistry.ts
2026-03-11 20:22:32 +01:00

159 lines
5.9 KiB
TypeScript

/**
* 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<NodeClassification, string> = {
psyche: 'Psyche',
pneuma: 'Pneuma',
physis: 'Physis',
archon: 'Archon',
}
export type NodeHelpEntry = {
title: string
content: React.ReactNode
}
export type NodeTypeDescriptor = {
id: string
component: React.ComponentType<any>
defaultStyle: { width: number; height: number }
defaultData: Record<string, unknown>
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<string, unknown>
/** Optional: data for Reset action; if omitted, getDefaultData(nodeId) or defaultData is used. */
getResetData?: (nodeId?: string) => Record<string, unknown>
/** 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<string, NodeTypeDescriptor>()
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<NodeClassification, NodeTypeDescriptor[]>()
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<string, unknown> {
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<string, unknown> {
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
}