lint: change folder names
This commit is contained in:
143
frontend/src/lib/graph/abstractNode.ts
Normal file
143
frontend/src/lib/graph/abstractNode.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Abstract node layer: shared types, hook, and factory for flow node components.
|
||||
*
|
||||
* - **AbstractNodeProps<TData>** — Typed props (id, data, width?, height?, selected?) for your node.
|
||||
* - **useAbstractNode(id, data)** — Flow context plus helpers: nodes, edges, setNodes, setEdges,
|
||||
* updateData(partial), incomingEdges, outgoingEdges, sourceIds, targetIds. Calling updateData()
|
||||
* also reports this node as a trigger for connection path (lifecycle "trigger").
|
||||
* - **createAbstractNodeComponent(displayName, Component)** — Wraps with memo + nodePropsAreEqual.
|
||||
*
|
||||
* **Node lifecycle / connection status:** Nodes that can be updating, paused, or in error should
|
||||
* call useSyncConnectionStatus(id, { updating, error, paused }) from @/lib/graph/nodeLifecycle so edge
|
||||
* colors and path animation stay correct. See nodeLifecycle.ts for the full contract.
|
||||
*
|
||||
* Example: define NodeData type, Props = AbstractNodeProps<NodeData>, use useAbstractNode in the
|
||||
* component, then export const MyNode = createAbstractNodeComponent('MyNode', MyNodeComponent).
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react'
|
||||
import FlowContext from './flowContext'
|
||||
import { nodePropsAreEqual } from './flowUtils'
|
||||
import type { AppNode } from './nodeTypes'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Props passed by React Flow to custom node components. Extend data with your node's shape. */
|
||||
export type AbstractNodeProps<TData = Record<string, unknown>> = {
|
||||
id: string
|
||||
data: TData
|
||||
width?: number
|
||||
height?: number
|
||||
selected?: boolean
|
||||
}
|
||||
|
||||
/** Edge shape used in flow context (minimal for connection logic). */
|
||||
export type FlowEdge = { id: string; source: string; target: string; [k: string]: unknown }
|
||||
|
||||
/** Node shape used in flow context (minimal for reading graph). */
|
||||
export type FlowNode = { id: string; type?: string; data?: unknown; position?: { x: number; y: number }; [k: string]: unknown }
|
||||
|
||||
/** Result of useAbstractNode: flow context plus helpers scoped to this node. */
|
||||
export type AbstractNodeContext<TData = Record<string, unknown>> = {
|
||||
id: string
|
||||
data: TData
|
||||
nodes: FlowNode[]
|
||||
edges: FlowEdge[]
|
||||
setNodes: (updater: (nodes: FlowNode[]) => FlowNode[]) => void
|
||||
setEdges: (updater: (edges: FlowEdge[]) => FlowEdge[]) => void
|
||||
/** Merge partial data into this node's data. Stable reference. */
|
||||
updateData: (partial: Partial<TData>) => void
|
||||
/** Incoming edge IDs (edges whose target is this node). */
|
||||
incomingEdges: FlowEdge[]
|
||||
/** Outgoing edge IDs (edges whose source is this node). */
|
||||
outgoingEdges: FlowEdge[]
|
||||
/** Source node IDs connected to this node (incoming). */
|
||||
sourceIds: string[]
|
||||
/** Target node IDs this node connects to (outgoing). */
|
||||
targetIds: string[]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Provides flow context and helpers for the current node. Use in any node component
|
||||
* that receives id and data; updateData(partial) merges into this node's data.
|
||||
*/
|
||||
export function useAbstractNode<TData = Record<string, unknown>>(
|
||||
id: string,
|
||||
data: TData
|
||||
): AbstractNodeContext<TData> {
|
||||
const ctx = useContext(FlowContext)
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const edges = ctx?.edges ?? []
|
||||
const setNodes = ctx?.setNodes
|
||||
const setEdges = ctx?.setEdges
|
||||
|
||||
const addConnectionPathTrigger = ctx?.addConnectionPathTrigger
|
||||
const updateData = useCallback(
|
||||
(partial: Partial<TData>) => {
|
||||
if (!setNodes) return
|
||||
setNodes((nds: AppNode[]) =>
|
||||
nds.map((n) =>
|
||||
n.id === id ? { ...n, data: { ...(n.data as object), ...partial } } : n
|
||||
) as AppNode[]
|
||||
)
|
||||
addConnectionPathTrigger?.(id)
|
||||
},
|
||||
[id, setNodes, addConnectionPathTrigger]
|
||||
)
|
||||
|
||||
const incomingEdges = useMemo(
|
||||
() => (edges as FlowEdge[]).filter((e) => e.target === id),
|
||||
[edges, id]
|
||||
)
|
||||
const outgoingEdges = useMemo(
|
||||
() => (edges as FlowEdge[]).filter((e) => e.source === id),
|
||||
[edges, id]
|
||||
)
|
||||
const sourceIds = useMemo(
|
||||
() => incomingEdges.map((e) => e.source).sort(),
|
||||
[incomingEdges]
|
||||
)
|
||||
const targetIds = useMemo(
|
||||
() => outgoingEdges.map((e) => e.target).sort(),
|
||||
[outgoingEdges]
|
||||
)
|
||||
|
||||
return {
|
||||
id,
|
||||
data,
|
||||
nodes,
|
||||
edges,
|
||||
setNodes: ((setNodes ?? (() => {})) as AbstractNodeContext<TData>['setNodes']),
|
||||
setEdges: setEdges ?? (() => {}),
|
||||
updateData,
|
||||
incomingEdges,
|
||||
outgoingEdges,
|
||||
sourceIds,
|
||||
targetIds,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component factory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Wraps a node component with React.memo and nodePropsAreEqual so only id/data/width/height/selected
|
||||
* changes trigger re-renders. Use with AbstractNodeProps<TData> for typed props.
|
||||
*/
|
||||
export function createAbstractNodeComponent<TData = Record<string, unknown>>(
|
||||
displayName: string,
|
||||
Component: React.ComponentType<AbstractNodeProps<TData>>
|
||||
): React.MemoExoticComponent<React.ComponentType<AbstractNodeProps<TData>>> {
|
||||
const Wrapped = React.memo(Component, nodePropsAreEqual) as React.MemoExoticComponent<
|
||||
React.ComponentType<AbstractNodeProps<TData>>
|
||||
>
|
||||
Wrapped.displayName = displayName
|
||||
return Wrapped
|
||||
}
|
||||
81
frontend/src/lib/graph/flowContext.tsx
Normal file
81
frontend/src/lib/graph/flowContext.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { Connection } from '@xyflow/react'
|
||||
import type { AppNode, AppEdge } from './nodeTypes'
|
||||
|
||||
export type ConnectionFrom = { nodeId: string; sourceHandle?: string } | null
|
||||
|
||||
/** Role of a node in the current connection path update: pushing data, receiving/loading, or just on path. */
|
||||
export type ConnectionPathRole = 'trigger' | 'updating' | 'on-path'
|
||||
|
||||
export type FlowActions = {
|
||||
pasteAtViewportCenter: () => void
|
||||
fitView: () => void
|
||||
}
|
||||
|
||||
export type FlowContextValue = {
|
||||
nodes: AppNode[]
|
||||
setNodes: (updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => void
|
||||
edges: AppEdge[]
|
||||
setEdges: (updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => void
|
||||
renamingNodeId: string | null
|
||||
setRenamingNodeId: (id: string | null) => void
|
||||
/** Set when user starts dragging from an output handle; cleared on connect end. Used to highlight valid targets. */
|
||||
connectionFrom: ConnectionFrom
|
||||
setConnectionFrom: (v: ConnectionFrom) => void
|
||||
isValidConnection: (connection: Connection) => boolean
|
||||
/** Set by FlowKeyboardShortcuts so Node menubar can trigger paste / fit view. */
|
||||
flowActionsRef: React.MutableRefObject<FlowActions | null>
|
||||
/** When set, graph centers on this node and a fullscreen dialog shows the node. Cleared on close. */
|
||||
fullscreenNodeId: string | null
|
||||
setFullscreenNodeId: (id: string | null) => void
|
||||
/** Node ids currently updating (e.g. render loading, agent running). Only edges on those paths show the ant trail. */
|
||||
connectionPathUpdatingNodeIds: string[]
|
||||
/** Node ids that triggered the current update (e.g. variable/config that changed). Path is restricted to downstream(trigger) ∩ upstream(updating). */
|
||||
connectionPathTriggerNodeIds: string[]
|
||||
/** Call when this node's output changed and may trigger downstream updates (e.g. variable value, config content). */
|
||||
addConnectionPathTrigger: (nodeId: string) => void
|
||||
/** All node ids on the path of an update. Edges with both endpoints in this set animate. */
|
||||
connectionPathNodeIds: Set<string>
|
||||
/** Path nodes in the "paused" segment (from trigger up to Archon on hold). Those edges are yellow. */
|
||||
connectionPathPausedSegmentNodeIds: Set<string>
|
||||
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
||||
connectionPathActiveSegmentNodeIds: Set<string>
|
||||
/** Archon-type nodes that are on hold (e.g. Agent waiting for Run). */
|
||||
connectionPathPausedNodeIds: string[]
|
||||
/** Add this node as paused (on hold); remove when user continues. */
|
||||
addConnectionPathPausedNode: (nodeId: string) => void
|
||||
/** Remove this node from paused. */
|
||||
removeConnectionPathPausedNode: (nodeId: string) => void
|
||||
/** Node ids that have an error (e.g. Render node). Incoming edges show error status (red). */
|
||||
connectionPathErrorNodeIds: string[]
|
||||
/** Call when this node has an error; remove when error is cleared. */
|
||||
addConnectionPathError: (nodeId: string) => void
|
||||
/** Call when this node's error is cleared. */
|
||||
removeConnectionPathError: (nodeId: string) => void
|
||||
/** Call when a path update starts for this node. Animation runs at least CONNECTION_PATH_UPDATE_MIN_MS. */
|
||||
startConnectionPathUpdate: (nodeId: string) => void
|
||||
/** Call when a path update ends for this node. If min duration not reached, animation continues until then. */
|
||||
endConnectionPathUpdate: (nodeId: string) => void
|
||||
}
|
||||
|
||||
const FlowContext = React.createContext<FlowContextValue | null>(null)
|
||||
|
||||
export default FlowContext
|
||||
|
||||
/**
|
||||
* Returns this node's role in the current path update for styling (pushing vs receiving).
|
||||
* Use with BaseNode's connectionPathRole prop or data-path-role for CSS.
|
||||
*/
|
||||
export function useConnectionPathRole(nodeId: string | undefined): ConnectionPathRole | null {
|
||||
const ctx = React.useContext(FlowContext)
|
||||
return useMemo(() => {
|
||||
if (!nodeId) return null
|
||||
const triggers = ctx?.connectionPathTriggerNodeIds
|
||||
const updating = ctx?.connectionPathUpdatingNodeIds
|
||||
const path = ctx?.connectionPathNodeIds
|
||||
if (!path?.has(nodeId)) return null
|
||||
if (triggers?.includes(nodeId)) return 'trigger'
|
||||
if (updating?.includes(nodeId)) return 'updating'
|
||||
return 'on-path'
|
||||
}, [nodeId, ctx?.connectionPathTriggerNodeIds, ctx?.connectionPathUpdatingNodeIds, ctx?.connectionPathNodeIds])
|
||||
}
|
||||
85
frontend/src/lib/graph/flowUtils.ts
Normal file
85
frontend/src/lib/graph/flowUtils.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Use as second argument to React.memo() for node components.
|
||||
* Skips re-render when only position (or other unrelated props) changed,
|
||||
* so dragging one node doesn't force other nodes to re-render.
|
||||
*/
|
||||
|
||||
import {
|
||||
getIdPrefix,
|
||||
getDefaultDataForType as getDefaultDataFromRegistry,
|
||||
getResetDataForType as getResetDataFromRegistry,
|
||||
} from './nodeRegistry'
|
||||
|
||||
export function nodePropsAreEqual<P extends { id?: string; data?: any; width?: number; height?: number; selected?: boolean }>(
|
||||
prev: P,
|
||||
next: P
|
||||
): boolean {
|
||||
return (
|
||||
prev.id === next.id &&
|
||||
prev.data === next.data &&
|
||||
prev.width === next.width &&
|
||||
prev.height === next.height &&
|
||||
prev.selected === next.selected
|
||||
)
|
||||
}
|
||||
|
||||
/** Next node id for type: prefix + 3-digit increasing number (001, 002, …). Uses nodeRegistry for prefix when available. */
|
||||
export function getNextNodeId(type: string, existingIds: string[]): string {
|
||||
const prefix = getIdPrefix(type)
|
||||
const re = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\d+)$`)
|
||||
let max = 0
|
||||
for (const id of existingIds) {
|
||||
const m = id.match(re)
|
||||
if (m) max = Math.max(max, parseInt(m[1], 10))
|
||||
}
|
||||
return `${prefix}${String(max + 1).padStart(3, '0')}`
|
||||
}
|
||||
|
||||
/** Recursively replace oldId with newId in string values (for rename propagation) */
|
||||
function replaceInData(value: unknown, oldId: string, newId: string): unknown {
|
||||
if (typeof value === 'string') return value.split(oldId).join(newId)
|
||||
if (value === null || typeof value !== 'object') return value
|
||||
if (Array.isArray(value)) return value.map((v) => replaceInData(v, oldId, newId))
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(value)) out[k] = replaceInData(v, oldId, newId)
|
||||
return out
|
||||
}
|
||||
|
||||
/** Update graph after renaming a node: change node id and all references in data and edges */
|
||||
export function replaceNodeIdInGraph(
|
||||
nodes: Array<{ id: string; data?: any; [k: string]: any }>,
|
||||
edges: Array<{ id: string; source: string; target: string; [k: string]: any }>,
|
||||
oldId: string,
|
||||
newId: string
|
||||
): { nodes: typeof nodes; edges: typeof edges } {
|
||||
const newNodes = nodes.map((n) =>
|
||||
n.id === oldId ? { ...n, id: newId } : { ...n, data: replaceInData(n.data, oldId, newId) as any }
|
||||
)
|
||||
const newEdges = edges.map((e) => ({
|
||||
...e,
|
||||
id: e.id.includes(oldId) ? e.id.split(oldId).join(newId) : e.id,
|
||||
source: e.source === oldId ? newId : e.source,
|
||||
target: e.target === oldId ? newId : e.target,
|
||||
}))
|
||||
return { nodes: newNodes, edges: newEdges }
|
||||
}
|
||||
|
||||
/** @deprecated Use getDefaultStyle from nodeRegistry. Kept for compatibility. */
|
||||
export const DEFAULT_NODE_STYLE: Record<string, { width: number; height: number }> = {
|
||||
config: { width: 320, height: 320 },
|
||||
render: { width: 384, height: 320 },
|
||||
variable: { width: 224, height: 180 },
|
||||
function: { width: 288, height: 260 },
|
||||
data: { width: 360, height: 280 },
|
||||
agent: { width: 360, height: 320 },
|
||||
}
|
||||
|
||||
/** Default data for a new node. Uses nodeRegistry when type is registered. */
|
||||
export function getDefaultDataForType(type: string, newId?: string): any {
|
||||
return getDefaultDataFromRegistry(type, newId)
|
||||
}
|
||||
|
||||
/** Data for Reset action. Uses nodeRegistry when type is registered. */
|
||||
export function getResetDataForType(type: string, nodeId?: string): any {
|
||||
return getResetDataFromRegistry(type, nodeId)
|
||||
}
|
||||
123
frontend/src/lib/graph/graphPath.ts
Normal file
123
frontend/src/lib/graph/graphPath.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Graph path utilities: compute which nodes/edges are "on the path" of an update.
|
||||
* Used to show connection ant trail only along the full chain (upstream → updating → downstream).
|
||||
* Works with any node types; any node can signal it is updating via startConnectionPathUpdate(id).
|
||||
*/
|
||||
|
||||
export type GraphEdge = { source: string; target: string }
|
||||
|
||||
/** Nodes reachable from seedIds by following edges forward (source → target). */
|
||||
export function getDownstreamNodeIds(edges: GraphEdge[], seedIds: string[]): Set<string> {
|
||||
const out = new Set<string>(seedIds)
|
||||
let added = true
|
||||
while (added) {
|
||||
added = false
|
||||
for (const e of edges) {
|
||||
if (out.has(e.source) && !out.has(e.target)) {
|
||||
out.add(e.target)
|
||||
added = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** Nodes that can reach any seed by following edges backward (target → source). */
|
||||
export function getUpstreamNodeIds(edges: GraphEdge[], seedIds: string[]): Set<string> {
|
||||
const out = new Set<string>(seedIds)
|
||||
let added = true
|
||||
while (added) {
|
||||
added = false
|
||||
for (const e of edges) {
|
||||
if (out.has(e.target) && !out.has(e.source)) {
|
||||
out.add(e.source)
|
||||
added = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* All node ids that lie on the path of an update.
|
||||
* - If updatingNodeIds non-empty: path = downstream(trigger) ∩ (upstream(updating) ∪ downstream(updating))
|
||||
* so we include config→agent→rendering when agent is running.
|
||||
* - Else if triggerNodeIds and pausedNodeIds non-empty: path = downstream(trigger) ∩ upstream(paused)
|
||||
* so we show yellow (config→agent) when config changed and agent is on hold.
|
||||
* An edge should show color iff both its source and target are in this set.
|
||||
*/
|
||||
export function getPathNodeIds(
|
||||
edges: GraphEdge[],
|
||||
updatingNodeIds: string[],
|
||||
triggerNodeIds?: string[],
|
||||
pausedNodeIds?: string[]
|
||||
): Set<string> {
|
||||
const hasUpdating = updatingNodeIds.length > 0
|
||||
const hasPausedPath =
|
||||
pausedNodeIds != null &&
|
||||
pausedNodeIds.length > 0 &&
|
||||
triggerNodeIds != null &&
|
||||
triggerNodeIds.length > 0
|
||||
|
||||
if (hasUpdating && triggerNodeIds != null && triggerNodeIds.length > 0) {
|
||||
const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds)
|
||||
const upstreamOfUpdating = getUpstreamNodeIds(edges, updatingNodeIds)
|
||||
const downstreamOfUpdating = getDownstreamNodeIds(edges, updatingNodeIds)
|
||||
const path = new Set<string>()
|
||||
downstreamOfTrigger.forEach((id) => {
|
||||
if (upstreamOfUpdating.has(id) || downstreamOfUpdating.has(id)) path.add(id)
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
if (hasPausedPath && !hasUpdating) {
|
||||
const upstream = getUpstreamNodeIds(edges, pausedNodeIds!)
|
||||
const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds!)
|
||||
const path = new Set<string>()
|
||||
upstream.forEach((id) => {
|
||||
if (downstreamOfTrigger.has(id)) path.add(id)
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
if (hasUpdating) {
|
||||
const upstream = getUpstreamNodeIds(edges, updatingNodeIds)
|
||||
const downstream = getDownstreamNodeIds(edges, updatingNodeIds)
|
||||
const path = new Set<string>(upstream)
|
||||
downstream.forEach((id) => path.add(id))
|
||||
return path
|
||||
}
|
||||
|
||||
return new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Path nodes from triggers up to and including the first paused node (Archon on hold).
|
||||
* Used to color those edges yellow; rest of path stays blue.
|
||||
*/
|
||||
export function getPausedSegmentNodeIds(
|
||||
edges: GraphEdge[],
|
||||
pathNodeIds: Set<string>,
|
||||
triggerNodeIds: string[],
|
||||
pausedNodeIds: string[]
|
||||
): Set<string> {
|
||||
if (pausedNodeIds.length === 0 || triggerNodeIds.length === 0) return new Set()
|
||||
const pausedSet = new Set(pausedNodeIds)
|
||||
const seeds = triggerNodeIds.filter((id) => pathNodeIds.has(id))
|
||||
if (seeds.length === 0) return new Set()
|
||||
const out = new Set<string>(seeds)
|
||||
const frontier: string[] = [...seeds]
|
||||
const visited = new Set<string>(seeds)
|
||||
while (frontier.length > 0) {
|
||||
const n = frontier.shift()!
|
||||
if (pausedSet.has(n)) continue
|
||||
for (const e of edges) {
|
||||
if (e.source !== n || !pathNodeIds.has(e.target) || visited.has(e.target)) continue
|
||||
visited.add(e.target)
|
||||
out.add(e.target)
|
||||
if (pausedSet.has(e.target)) continue
|
||||
frontier.push(e.target)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
110
frontend/src/lib/graph/nodeHelp.tsx
Normal file
110
frontend/src/lib/graph/nodeHelp.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import type React from 'react'
|
||||
|
||||
export type NodeType = 'config' | 'render' | 'variable' | 'function' | 'data' | 'agent'
|
||||
|
||||
export type NodeHelpEntry = {
|
||||
title: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
const Code = ({ children }: { children: React.ReactNode }) => (
|
||||
<code className="rounded bg-muted px-1 py-0.5 text-xs font-mono">{children}</code>
|
||||
)
|
||||
|
||||
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (
|
||||
<div className="mt-3 first:mt-0">
|
||||
<h4 className="text-xs font-semibold text-foreground">{title}</h4>
|
||||
<div className="mt-1 text-xs text-muted-foreground">{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const NODE_HELP: Record<NodeType, NodeHelpEntry> = {
|
||||
config: {
|
||||
title: 'Config node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Config nodes hold PlantUML + Nunjucks template content. Connect one config to a Render node to display the diagram. Use the editor to write <Code>@startuml</Code> blocks and Nunjucks tags (<Code>{'{{ }}'}</Code>, <Code>{'{% %}'}</Code>).</p>
|
||||
</Section>
|
||||
<Section title="Using in another node">
|
||||
<p>From another config, reference this template:</p>
|
||||
<ul className="list-disc pl-4 mt-1 space-y-0.5">
|
||||
<li><Code>{'{% extends "configId" %}'}</Code> — inherit layout</li>
|
||||
<li><Code>{'{% include "configId" %}'}</Code> — inline content</li>
|
||||
<li><Code>{'{% import "configId" as alias %}'}</Code> — use as macro namespace</li>
|
||||
</ul>
|
||||
<p className="mt-2">Replace <Code>configId</Code> with this node’s id or its title. Connect variables/functions to this config; they are available as <Code>{'{{ varId }}'}</Code> and <Code>{'{{ x | fnId }}'}</Code> in the template.</p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
render: {
|
||||
title: 'Renderer node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Connect a single Config node (input) to this Render node. It resolves the config’s PlantUML + Nunjucks (variables, function filters, extends/include), sends the result to the diagram service, and shows the SVG here.</p>
|
||||
</Section>
|
||||
<Section title="Using in another node">
|
||||
<p>Renderer nodes are terminal: they only consume configs. They are not referenced from other nodes. To reuse a diagram, reference the Config node from another Config (extends/include), then connect that config to a Render node.</p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
variable: {
|
||||
title: 'Variable node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Variables hold a value (string, number, or boolean). Connect a Variable node to a Config node to expose it in that config’s template.</p>
|
||||
</Section>
|
||||
<Section title="Using in another node">
|
||||
<p>In a Config template connected to this variable, use <Code>{'{{ '}<em>nodeId</em>{' }}'}</Code> where <em>nodeId</em> is this node’s id. Example: if the variable node id is <Code>var_001</Code>, write <Code>{'{{ var_001 }}'}</Code> in the config.</p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
function: {
|
||||
title: 'Function node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Functions are Nunjucks custom filters. Write a body using either named parameters (<Code>function(num, x, kwargs) { ... }</Code>) or the <Code>args</Code> array. Connect this node to a Config to use the filter in that config’s template.</p>
|
||||
</Section>
|
||||
<Section title="Using in another node">
|
||||
<p>In a Config template, use the filter syntax: <Code>{'{{ value | '}<em>nodeId</em>{' }}'}</Code> or <Code>{'{{ value | '}<em>nodeId</em>{'(arg1, key=val) }}'}</Code>. The first argument is the value before <Code>|</Code>; extra arguments and keyword args are passed as in Nunjucks. Return a value or a Promise for async filters.</p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
data: {
|
||||
title: 'Data node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Data nodes hold CSV data. Drop a .csv file onto the node to load it; the table is shown in the node. Connect this node to a Config node to use the data in Nunjucks templates.</p>
|
||||
</Section>
|
||||
<Section title="Using in templates">
|
||||
<p>In a Config template connected to this data node, the variable <Code>{'{{ '}<em>nodeId</em>{' }}'}</Code> is an array of row objects (one per CSV row). Each row has keys from the CSV header. Example: <Code>{'{% for row in data_001 %}{{ row.name }}, {{ row.value }}{% endfor %}'}</Code></p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
agent: {
|
||||
title: 'Agent node',
|
||||
content: (
|
||||
<>
|
||||
<Section title="How to use">
|
||||
<p>Agent nodes use AI to produce structured markdown. Connect Config nodes as the <strong>prompt</strong> (their content is sent as the main prompt). Optionally connect Variable or Data nodes; they are passed as context. Define additional context in the text area. Click Run to execute the agent; output is markdown. Connect this node to a Renderer to display the result.</p>
|
||||
</Section>
|
||||
<Section title="Output">
|
||||
<p>When connected to a Renderer node, the agent’s markdown output is rendered there. Ensure the backend <Code>/api/agent</Code> is running and (if using OpenAI) <Code>OPENAI_API_KEY</Code> is set.</p>
|
||||
</Section>
|
||||
</>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export function getNodeHelp(nodeType: NodeType): NodeHelpEntry {
|
||||
return NODE_HELP[nodeType] ?? { title: nodeType, content: null }
|
||||
}
|
||||
98
frontend/src/lib/graph/nodeLifecycle.ts
Normal file
98
frontend/src/lib/graph/nodeLifecycle.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Node lifecycle: contract that nodes implement so the graph can show the right
|
||||
* connection status (edge colors) and path animation.
|
||||
*
|
||||
* ## Lifecycle phases (conceptual)
|
||||
*
|
||||
* - **Idle** – Node is not on an active path; edges to/from it use default style.
|
||||
* - **Trigger** – Node's output changed (e.g. config content, variable value). Reported
|
||||
* automatically when the node calls `updateData()` from useAbstractNode. Downstream
|
||||
* path is computed from triggers + updating nodes.
|
||||
* - **Updating** – Node is doing async work (e.g. agent running, renderer loading).
|
||||
* Report `updating: true` at start, `updating: false` when done. Incoming/outgoing
|
||||
* edges on the path show "updating" (blue).
|
||||
* - **Paused** – Node is on hold (e.g. agent waiting for Run after inputs changed).
|
||||
* Report `paused: true` when waiting, `paused: false` when not. Edges in the paused
|
||||
* segment show "paused" (yellow).
|
||||
* - **Error** – Node has an error to show. Report `error: true` when error is set,
|
||||
* `error: false` when cleared. Incoming edges to this node show "error" (red).
|
||||
*
|
||||
* ## Connection status integration
|
||||
*
|
||||
* Edge status is derived in getConnectionStatus() from the sets that this lifecycle
|
||||
* feeds: connectionPathUpdatingNodeIds, connectionPathPausedNodeIds,
|
||||
* connectionPathErrorNodeIds (plus path/paused segment from graphPath). Priority:
|
||||
* error > paused > updating > default.
|
||||
*
|
||||
* Nodes that can be updating, paused, or in error should call useSyncConnectionStatus()
|
||||
* with their current state so edges update correctly.
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useRef } from 'react'
|
||||
import FlowContext from './flowContext'
|
||||
|
||||
export type NodeLifecyclePhase = 'idle' | 'trigger' | 'updating' | 'paused' | 'error'
|
||||
|
||||
/**
|
||||
* State that drives connection status for this node.
|
||||
* Pass the current values from your node; the hook syncs them to FlowContext.
|
||||
*/
|
||||
export type NodeConnectionStatusState = {
|
||||
/** Node is doing async work (e.g. loading, running). Incoming/outgoing path edges show blue. */
|
||||
updating?: boolean
|
||||
/** Node has an error. Incoming edges to this node show red. */
|
||||
error?: boolean
|
||||
/** Node is on hold (e.g. agent waiting for Run). Path edges in paused segment show yellow. */
|
||||
paused?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs this node's lifecycle state to FlowContext so connection status (edge colors)
|
||||
* and path animation are correct. Call once per node with the current updating/error/paused
|
||||
* state; the hook will add/remove this node from the appropriate sets.
|
||||
*
|
||||
* Use in any node that can be updating, in error, or paused:
|
||||
*
|
||||
* const [loading, setLoading] = useState(false)
|
||||
* const [error, setError] = useState(null)
|
||||
* const hasPendingInputs = ...
|
||||
* useSyncConnectionStatus(id, { updating: loading, error: !!error, paused: hasPendingInputs })
|
||||
*/
|
||||
export function useSyncConnectionStatus(
|
||||
nodeId: string,
|
||||
state: NodeConnectionStatusState
|
||||
): void {
|
||||
const ctx = useContext(FlowContext)
|
||||
const { updating, error, paused } = state
|
||||
const prevRef = useRef({ updating: false, error: false, paused: false })
|
||||
|
||||
useEffect(() => {
|
||||
const prev = prevRef.current
|
||||
const nowUpdating = Boolean(updating)
|
||||
const nowError = Boolean(error)
|
||||
const nowPaused = Boolean(paused)
|
||||
|
||||
if (prev.updating !== nowUpdating) {
|
||||
if (nowUpdating) ctx?.startConnectionPathUpdate?.(nodeId)
|
||||
else ctx?.endConnectionPathUpdate?.(nodeId)
|
||||
prev.updating = nowUpdating
|
||||
}
|
||||
if (prev.error !== nowError) {
|
||||
if (nowError) ctx?.addConnectionPathError?.(nodeId)
|
||||
else ctx?.removeConnectionPathError?.(nodeId)
|
||||
prev.error = nowError
|
||||
}
|
||||
if (prev.paused !== nowPaused) {
|
||||
if (nowPaused) ctx?.addConnectionPathPausedNode?.(nodeId)
|
||||
else ctx?.removeConnectionPathPausedNode?.(nodeId)
|
||||
prev.paused = nowPaused
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (prevRef.current.updating) ctx?.endConnectionPathUpdate?.(nodeId)
|
||||
if (prevRef.current.error) ctx?.removeConnectionPathError?.(nodeId)
|
||||
if (prevRef.current.paused) ctx?.removeConnectionPathPausedNode?.(nodeId)
|
||||
prevRef.current = { updating: false, error: false, paused: false }
|
||||
}
|
||||
}, [nodeId, updating, error, paused, ctx])
|
||||
}
|
||||
164
frontend/src/lib/graph/nodeRegistry.ts
Normal file
164
frontend/src/lib/graph/nodeRegistry.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Extensible node type registry. Register node types with registerNodeType() or use NodeTypeBuilder.
|
||||
* Built-in types are registered in registerBuiltinNodes.ts.
|
||||
* Use getRegisteredNodeTypes() / getNodeType(id) for defaults, validation, and UI.
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import { registerSourceRenderingLogic } from '../sourceRenderingLogic'
|
||||
import type { SourceRenderingLogic } from '../sourceRenderingLogic'
|
||||
|
||||
/** 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
|
||||
/** When set, this type can feed the Renderer; registration will also register source rendering logic. */
|
||||
sourceRenderingLogic?: SourceRenderingLogic
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
if (descriptor.sourceRenderingLogic) {
|
||||
registerSourceRenderingLogic(descriptor.id, descriptor.sourceRenderingLogic)
|
||||
}
|
||||
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
|
||||
}
|
||||
165
frontend/src/lib/graph/nodeTypeBuilder.ts
Normal file
165
frontend/src/lib/graph/nodeTypeBuilder.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* Fluent builder for NodeTypeDescriptor. Use to define node types with common behavior
|
||||
* and optional source rendering logic (for types that feed the Renderer).
|
||||
*
|
||||
* Example:
|
||||
* createNodeTypeBuilder('config', ConfigNode, { width: 320, height: 320 }, { configType: 'plantuml', content: '', title: '' })
|
||||
* .idPrefix('cfg_')
|
||||
* .withInputOutput(true, true)
|
||||
* .classification('psyche')
|
||||
* .allowedSourceTypes(['config', 'variable', 'function', 'data'])
|
||||
* .allowedTargetTypes(['config', 'render', 'agent'])
|
||||
* .help(NODE_HELP.config)
|
||||
* .menu('Config', <ScrollText />)
|
||||
* .connectionLabel('adding input')
|
||||
* .withFullscreen()
|
||||
* .sourceRenderingLogic({ defaultUpdateMode: 'auto', getResolvedContent: ... })
|
||||
* .build()
|
||||
*/
|
||||
|
||||
import type React from 'react'
|
||||
import type { NodeTypeDescriptor, NodeClassification, NodeHelpEntry } from './nodeRegistry'
|
||||
import type { SourceRenderingLogic } from '../sourceRenderingLogic'
|
||||
|
||||
type OptionalDescriptor = Partial<
|
||||
Omit<
|
||||
NodeTypeDescriptor,
|
||||
'id' | 'component' | 'defaultStyle' | 'defaultData' | 'idPrefix' | 'hasInput' | 'hasOutput' | 'help' | 'menuLabel' | 'menuIcon'
|
||||
>
|
||||
>
|
||||
|
||||
export class NodeTypeBuilder {
|
||||
private readonly id: string
|
||||
private readonly component: React.ComponentType<any>
|
||||
private readonly defaultStyle: { width: number; height: number }
|
||||
private readonly defaultData: Record<string, unknown>
|
||||
private partial: OptionalDescriptor & {
|
||||
idPrefix?: string
|
||||
hasInput?: boolean
|
||||
hasOutput?: boolean
|
||||
help?: NodeHelpEntry
|
||||
menuLabel?: string
|
||||
menuIcon?: React.ReactNode
|
||||
} = {}
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
component: React.ComponentType<any>,
|
||||
defaultStyle: { width: number; height: number },
|
||||
defaultData: Record<string, unknown>
|
||||
) {
|
||||
this.id = id
|
||||
this.component = component
|
||||
this.defaultStyle = defaultStyle
|
||||
this.defaultData = defaultData
|
||||
}
|
||||
|
||||
idPrefix(prefix: string): this {
|
||||
this.partial.idPrefix = prefix
|
||||
return this
|
||||
}
|
||||
|
||||
withInputOutput(hasInput: boolean, hasOutput: boolean): this {
|
||||
this.partial.hasInput = hasInput
|
||||
this.partial.hasOutput = hasOutput
|
||||
return this
|
||||
}
|
||||
|
||||
classification(c: NodeClassification): this {
|
||||
this.partial.classification = c
|
||||
return this
|
||||
}
|
||||
|
||||
allowedSourceTypes(types: string[]): this {
|
||||
this.partial.allowedSourceTypes = types
|
||||
return this
|
||||
}
|
||||
|
||||
allowedTargetTypes(types: string[]): this {
|
||||
this.partial.allowedTargetTypes = types
|
||||
return this
|
||||
}
|
||||
|
||||
help(help: NodeHelpEntry): this {
|
||||
this.partial.help = help
|
||||
return this
|
||||
}
|
||||
|
||||
menu(label: string, icon: React.ReactNode): this {
|
||||
this.partial.menuLabel = label
|
||||
this.partial.menuIcon = icon
|
||||
return this
|
||||
}
|
||||
|
||||
getDefaultData(fn: (newId?: string) => Record<string, unknown>): this {
|
||||
this.partial.getDefaultData = fn
|
||||
return this
|
||||
}
|
||||
|
||||
getResetData(fn: (nodeId?: string) => Record<string, unknown>): this {
|
||||
this.partial.getResetData = fn
|
||||
return this
|
||||
}
|
||||
|
||||
connectionLabel(label: string): this {
|
||||
this.partial.connectionLabel = label
|
||||
return this
|
||||
}
|
||||
|
||||
withFullscreen(): this {
|
||||
this.partial.supportsFullscreen = true
|
||||
return this
|
||||
}
|
||||
|
||||
sourceRenderingLogic(logic: SourceRenderingLogic): this {
|
||||
this.partial.sourceRenderingLogic = logic
|
||||
return this
|
||||
}
|
||||
|
||||
build(): NodeTypeDescriptor {
|
||||
const {
|
||||
idPrefix,
|
||||
hasInput,
|
||||
hasOutput,
|
||||
help,
|
||||
menuLabel,
|
||||
menuIcon,
|
||||
sourceRenderingLogic,
|
||||
...rest
|
||||
} = this.partial
|
||||
if (idPrefix == null || hasInput == null || hasOutput == null || !help || !menuLabel || menuIcon == null) {
|
||||
throw new Error(
|
||||
`NodeTypeBuilder.build(): missing required fields for "${this.id}". Set idPrefix, withInputOutput, help, and menu.`
|
||||
)
|
||||
}
|
||||
const descriptor: NodeTypeDescriptor = {
|
||||
id: this.id,
|
||||
component: this.component,
|
||||
defaultStyle: this.defaultStyle,
|
||||
defaultData: this.defaultData,
|
||||
idPrefix,
|
||||
hasInput,
|
||||
hasOutput,
|
||||
help,
|
||||
menuLabel,
|
||||
menuIcon,
|
||||
...rest,
|
||||
}
|
||||
if (sourceRenderingLogic != null) {
|
||||
descriptor.sourceRenderingLogic = sourceRenderingLogic
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building a node type descriptor. Required chain: idPrefix, withInputOutput, help, menu, then build().
|
||||
*/
|
||||
export function createNodeTypeBuilder(
|
||||
id: string,
|
||||
component: React.ComponentType<any>,
|
||||
defaultStyle: { width: number; height: number },
|
||||
defaultData: Record<string, unknown>
|
||||
): NodeTypeBuilder {
|
||||
return new NodeTypeBuilder(id, component, defaultStyle, defaultData)
|
||||
}
|
||||
15
frontend/src/lib/graph/nodeTypes.ts
Normal file
15
frontend/src/lib/graph/nodeTypes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Central node and edge types for the app. Use AppNode / AppEdge in graph state and context.
|
||||
*/
|
||||
|
||||
import type { Node, Edge } from '@xyflow/react'
|
||||
import type { ConfigNodeData } from '@/components/nodes/config'
|
||||
import type { RenderingNodeData } from '@/components/nodes/render'
|
||||
import type { VariableNodeData } from '@/components/nodes/variable'
|
||||
import type { FunctionNodeData } from '@/components/nodes/function'
|
||||
import type { DataNodeData } from '@/components/nodes/data'
|
||||
import type { AgentNodeData } from '@/components/nodes/agent'
|
||||
|
||||
export type AppNodeData = ConfigNodeData | RenderingNodeData | VariableNodeData | FunctionNodeData | DataNodeData | AgentNodeData
|
||||
export type AppNode = Node<AppNodeData>
|
||||
export type AppEdge = Edge
|
||||
22
frontend/src/lib/graph/registerBuiltinNodes.tsx
Normal file
22
frontend/src/lib/graph/registerBuiltinNodes.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Registers built-in node types using the node type builder.
|
||||
* Each node type is defined in its own folder under components/nodes/<type>/ and
|
||||
* provides a getXxxNodeDescriptor() that uses the builder (including optional source rendering logic).
|
||||
*/
|
||||
|
||||
import { registerNodeType } from './nodeRegistry'
|
||||
import { getConfigNodeDescriptor } from '@/components/nodes/config'
|
||||
import { getAgentNodeDescriptor } from '@/components/nodes/agent'
|
||||
import { getRenderNodeDescriptor } from '@/components/nodes/render'
|
||||
import { getVariableNodeDescriptor } from '@/components/nodes/variable'
|
||||
import { getFunctionNodeDescriptor } from '@/components/nodes/function'
|
||||
import { getDataNodeDescriptor } from '@/components/nodes/data'
|
||||
|
||||
export function registerBuiltinNodes(): void {
|
||||
registerNodeType(getConfigNodeDescriptor())
|
||||
registerNodeType(getAgentNodeDescriptor())
|
||||
registerNodeType(getRenderNodeDescriptor())
|
||||
registerNodeType(getVariableNodeDescriptor())
|
||||
registerNodeType(getFunctionNodeDescriptor())
|
||||
registerNodeType(getDataNodeDescriptor())
|
||||
}
|
||||
Reference in New Issue
Block a user