From bfd1a40332da6f2bc7049ea52d69fbc69bd5e747 Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 12 Mar 2026 17:23:47 +0100 Subject: [PATCH] refactoring --- frontend/docs/IMPROVEMENTS.md | 23 +++-- frontend/src/app/canvas/CanvasPage.tsx | 85 ++----------------- frontend/src/app/canvas/canvasGraphUtils.ts | 66 ++++++++++++++ frontend/src/app/canvas/useCanvasGraph.ts | 33 +++++++ .../components/nodes/config/ConfigNode.tsx | 4 +- .../components/nodes/config/renderingLogic.ts | 15 ++-- .../nodes/render/useRenderingNodeState.ts | 10 +-- frontend/src/lib/graph/configTypes.ts | 40 +++++++-- frontend/src/lib/graph/rendering.ts | 2 +- frontend/src/lib/graph/renderingSignatures.ts | 7 +- .../src/lib/graph/sourceRenderingLogic.ts | 7 +- frontend/src/lib/graph/templateRefs.ts | 5 +- frontend/src/main.tsx | 2 + 13 files changed, 179 insertions(+), 120 deletions(-) create mode 100644 frontend/src/app/canvas/canvasGraphUtils.ts create mode 100644 frontend/src/app/canvas/useCanvasGraph.ts diff --git a/frontend/docs/IMPROVEMENTS.md b/frontend/docs/IMPROVEMENTS.md index e0924ef..7baa255 100644 --- a/frontend/docs/IMPROVEMENTS.md +++ b/frontend/docs/IMPROVEMENTS.md @@ -31,6 +31,23 @@ This doc summarizes recent improvements and suggested next steps for readability - **CanvasContextMenuContent** (`app/canvas/CanvasContextMenuContent.tsx`): context menu content (Create Node grouped by classification, Paste). CanvasPage passes `onCreateNode` and `onPaste`. - **useCanvasConnectionPath** (`app/canvas/useCanvasConnectionPath.ts`): all connection-path state and callbacks (updating/trigger/paused/error node ids, path node ids, start/end update, add/remove paused/error). CanvasPage calls the hook with `edges` and passes the result into FlowContext. +### 6. useCanvasGraph and canvas graph utils + +- **canvasGraphUtils.ts**: `getExampleGraph()`, `getInitialGraph(projectId)` (load from storage or return example). Example nodes/edges live here. +- **useCanvasGraph(projectId)**: wraps `useGraphStateWithHistory` with initial graph from `getInitialGraph(projectId)` and debounced save to storage when `projectId` is set. CanvasPage uses this instead of inline state + persistence. + +### 7. Config types registry + +- **configTypes.ts**: `configTypeRegistry` (Map), `registerConfigType(type)`, `getConfigTypes()`, `getConfigTypeIds()`, `registerBuiltinConfigTypes()`. Built-in types are in `BUILTIN_CONFIG_TYPES` and registered at app init. `getConfigType(id)` looks up in the registry; `getConfigTypeId(data)` uses registered ids. ConfigNode uses `getConfigTypes()` instead of `CONFIG_TYPES`. New output types can call `registerConfigType()` without editing the core array. + +### 8. Consistent NodeLike / EdgeLike + +- **templateRefs.ts**: `NodeLike` includes `type?: string`; both types documented. Single source of truth for minimal node/edge shape in the graph lib. +- **sourceRenderingLogic**: context uses `NodeLike[]` and `EdgeLike[]` from templateRefs. +- **renderingSignatures**: imports and re-exports `NodeLike` / `EdgeLike` from templateRefs; no local duplicate types. +- **config/renderingLogic**: uses context nodes/edges directly (no casts); `setVarInContext(src: NodeLike)`. +- **useRenderingNodeState**: casts to `NodeLike[]` / `EdgeLike[]` when calling `buildSourceSignatures` (types from renderingSignatures). + ## Design patterns in use | Pattern | Where | @@ -43,8 +60,4 @@ This doc summarizes recent improvements and suggested next steps for readability ## Suggested next steps -1. **CanvasPage**: Further split optional: e.g. `useCanvasGraph()` for graph state + persistence + connection rules, so the page is mostly composition and layout. - -2. **Config types**: If you add more output types (e.g. Mermaid), consider a small registry API (`registerConfigType`, `getConfigType`) instead of a single large `CONFIG_TYPES` array. - -3. **Consistent node shape in lib**: `templateRefs` and `renderingSignatures` use `EdgeLike` / `NodeLike`; standardize where appropriate to reduce casts. +- **CanvasPage**: Optional further split (e.g. move connection validation or node/edge change handlers into a hook) if the file grows again. diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index 1c1ee3c..8c36993 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -26,7 +26,8 @@ import { AnimatedEdge } from '@/components/graph/AnimatedEdge' import FlowContext from '@/lib/graph/flowContext' import { useTheme } from '@/lib/themeContext' import { usePlatform } from '@/app/kosmos/KosmosContext' -import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory' +import { useCanvasGraph } from '@/app/canvas/useCanvasGraph' +import { getExampleGraph } from '@/app/canvas/canvasGraphUtils' import { ContextMenu, ContextMenuTrigger } from '@/components/ui/context-menu' import { CanvasContextMenuContent } from '@/app/canvas/CanvasContextMenuContent' import { useCanvasConnectionPath } from '@/app/canvas/useCanvasConnectionPath' @@ -48,7 +49,7 @@ import { DialogTitle, } from '@/components/ui/dialog' import { FolderOpen, FileStack, CircleDotDashed } from 'lucide-react' -import { DEFAULT_NODE_STYLE, getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils' +import { getDefaultDataForType, getNextNodeId } from '@/lib/graph/flowUtils' import { getRegisteredNodeTypes, getRegisteredNodeTypeIds, @@ -58,12 +59,7 @@ import { } from '@/lib/graph/nodeRegistry' import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' import { toast } from 'sonner' -import { - loadGraphFromStorage, - saveGraphToStorage, - PROJECT_FILE_EXT, - PROJECT_VERSION, -} from '@/app/pleroma/projectGraphStorage' +import { PROJECT_FILE_EXT, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' const SNAP_GRID: [number, number] = [15, 15] const DUPLICATE_OFFSET = { x: 30, y: 30 } @@ -72,52 +68,6 @@ const snapToGrid = (x: number, y: number): { x: number; y: number } => ({ y: Math.round(y / SNAP_GRID[1]) * SNAP_GRID[1], }) -const NODE_GAP = 150 -const initialNodes: AppNode[] = [ - { - id: 'var_001', - position: { x: 50, y: 100 }, - data: { value: 'Zoe', valueType: 'string' as const }, - type: 'variable', - style: DEFAULT_NODE_STYLE.variable, - }, - { - id: 'cfg_001', - position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 }, - data: { - plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n', - title: 'config-cfg_001', - }, - type: 'config', - style: DEFAULT_NODE_STYLE.config, - }, - { - id: 'rnd_001', - position: { - x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP, - y: 50, - }, - data: {}, - type: 'render', - style: DEFAULT_NODE_STYLE.render, - }, -] - -const initialEdges: AppEdge[] = [ - { id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' }, - { id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' }, -] - -function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } { - return { - nodes: initialNodes.map((n) => ({ - ...n, - data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data, - })), - edges: initialEdges.map((e) => ({ ...e })), - } -} - function FlowFitViewOnLoad() { const nodesInitialized = useNodesInitialized() const { fitView } = useReactFlow() @@ -196,21 +146,9 @@ export type CanvasPageProps = { projectId?: string } -function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } { - if (projectId) { - const stored = loadGraphFromStorage(projectId) - if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) { - return { nodes: stored.nodes as AppNode[], edges: stored.edges as AppEdge[] } - } - return { nodes: [], edges: [] } - } - return getExampleGraph() -} - export function CanvasPage({ projectId }: CanvasPageProps) { const { theme } = useTheme() const { showMinimap } = usePlatform() - const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId]) const { nodes, edges, @@ -225,20 +163,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) { canUndo, canRedo, setStateImmediate, - } = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges) - - // Persist graph to localStorage when projectId is set (debounced) - const saveTimeoutRef = React.useRef | null>(null) - React.useEffect(() => { - if (!projectId) return - const save = () => { - saveGraphToStorage(projectId, { version: PROJECT_VERSION, nodes, edges }) - } - saveTimeoutRef.current = setTimeout(save, 500) - return () => { - if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) - } - }, [projectId, nodes, edges]) + } = useCanvasGraph(projectId) const importInputRef = useRef(null) const [rfInstance, setRfInstance] = React.useState(null) diff --git a/frontend/src/app/canvas/canvasGraphUtils.ts b/frontend/src/app/canvas/canvasGraphUtils.ts new file mode 100644 index 0000000..f5fa9d6 --- /dev/null +++ b/frontend/src/app/canvas/canvasGraphUtils.ts @@ -0,0 +1,66 @@ +/** + * Canvas graph: example graph and initial graph from storage. + * Used by useCanvasGraph and CanvasPage (e.g. Load example). + */ + +import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' +import { DEFAULT_NODE_STYLE } from '@/lib/graph/flowUtils' +import { loadGraphFromStorage } from '@/app/pleroma/projectGraphStorage' + +const NODE_GAP = 150 + +const EXAMPLE_NODES: AppNode[] = [ + { + id: 'var_001', + position: { x: 50, y: 100 }, + data: { value: 'Zoe', valueType: 'string' as const }, + type: 'variable', + style: DEFAULT_NODE_STYLE.variable, + }, + { + id: 'cfg_001', + position: { x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP, y: 50 }, + data: { + plantuml: '@startuml\nactor User\nparticipant "{{ var_001 }}" as R\nUser -> R : loves\n@enduml\n', + title: 'config-cfg_001', + }, + type: 'config', + style: DEFAULT_NODE_STYLE.config, + }, + { + id: 'rnd_001', + position: { + x: 50 + DEFAULT_NODE_STYLE.variable.width + NODE_GAP + DEFAULT_NODE_STYLE.config.width + NODE_GAP, + y: 50, + }, + data: {}, + type: 'render', + style: DEFAULT_NODE_STYLE.render, + }, +] + +const EXAMPLE_EDGES: AppEdge[] = [ + { id: 'e-var_001-cfg_001', source: 'var_001', target: 'cfg_001', type: 'animated' }, + { id: 'e-cfg_001-rnd_001', source: 'cfg_001', target: 'rnd_001', type: 'animated' }, +] + +export function getExampleGraph(): { nodes: AppNode[]; edges: AppEdge[] } { + return { + nodes: EXAMPLE_NODES.map((n) => ({ + ...n, + data: n.data && typeof n.data === 'object' ? { ...(n.data as object) } : n.data, + })), + edges: EXAMPLE_EDGES.map((e) => ({ ...e })), + } +} + +export function getInitialGraph(projectId: string | undefined): { nodes: AppNode[]; edges: AppEdge[] } { + if (projectId) { + const stored = loadGraphFromStorage(projectId) + if (stored && (stored.nodes.length > 0 || stored.edges.length > 0)) { + return { nodes: stored.nodes as AppNode[], edges: stored.edges as AppEdge[] } + } + return { nodes: [], edges: [] } + } + return getExampleGraph() +} diff --git a/frontend/src/app/canvas/useCanvasGraph.ts b/frontend/src/app/canvas/useCanvasGraph.ts new file mode 100644 index 0000000..b170022 --- /dev/null +++ b/frontend/src/app/canvas/useCanvasGraph.ts @@ -0,0 +1,33 @@ +/** + * Hook for canvas graph state and persistence. Wraps useGraphStateWithHistory + * with initial graph from project storage (or example) and debounced save. + * Keeps CanvasPage focused on composition and layout. + */ + +import { useEffect, useMemo, useRef } from 'react' +import { useGraphStateWithHistory } from '@/hooks/useGraphStateWithHistory' +import { getInitialGraph } from '@/app/canvas/canvasGraphUtils' +import { saveGraphToStorage, PROJECT_VERSION } from '@/app/pleroma/projectGraphStorage' +import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' + +export type UseCanvasGraphResult = ReturnType + +export function useCanvasGraph(projectId: string | undefined): UseCanvasGraphResult { + const initialGraph = useMemo(() => getInitialGraph(projectId), [projectId]) + const result = useGraphStateWithHistory(initialGraph.nodes, initialGraph.edges) + const { nodes, edges } = result + + const saveTimeoutRef = useRef | null>(null) + useEffect(() => { + if (!projectId) return + const save = () => { + saveGraphToStorage(projectId, { version: PROJECT_VERSION, nodes, edges }) + } + saveTimeoutRef.current = setTimeout(save, 500) + return () => { + if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current) + } + }, [projectId, nodes, edges]) + + return result +} diff --git a/frontend/src/components/nodes/config/ConfigNode.tsx b/frontend/src/components/nodes/config/ConfigNode.tsx index 8a05871..8c352ab 100644 --- a/frontend/src/components/nodes/config/ConfigNode.tsx +++ b/frontend/src/components/nodes/config/ConfigNode.tsx @@ -14,7 +14,7 @@ import { nunjucksCompletionSource } from '@/lib/nunjucksAutocomplete' import { plantumlLanguage } from '@/lib/plantumlLanguage' import { useTheme } from '@/lib/themeContext' import { - CONFIG_TYPES, + getConfigTypes, getConfigContent, getConfigType, getConfigTypeId, @@ -261,7 +261,7 @@ function ConfigNodeComponent({ id, data, width, height, selected }: Props) { - {CONFIG_TYPES.map((t) => ( + {getConfigTypes().map((t) => ( {t.label} diff --git a/frontend/src/components/nodes/config/renderingLogic.ts b/frontend/src/components/nodes/config/renderingLogic.ts index 9ac4145..0b7a9b6 100644 --- a/frontend/src/components/nodes/config/renderingLogic.ts +++ b/frontend/src/components/nodes/config/renderingLogic.ts @@ -7,10 +7,7 @@ import nunjucks from 'nunjucks' import type { ResolvedContentResult, SourceRenderingLogicContext } from '@/lib/graph/rendering' import { getConfigContent, getConfigType, getConfigTypeId, type ConfigTypeId } from '@/lib/graph/rendering' -import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs' - -type Node = { id: string; type?: string; data?: unknown } -type Edge = { id: string; source: string; target: string } +import { isReachable, resolveExtendsRef, getTemplateRefs, type NodeLike } from '@/lib/graph/templateRefs' export async function getResolvedContentForConfig(context: SourceRenderingLogicContext): Promise { const { nodes, edges, sourceNodeId, renderNodeId } = context @@ -24,11 +21,11 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC const configIdsUsed = new Set() const addConfigAndRefs = (templateName: string, visited = new Set()) => { - const refId = resolveExtendsRef(nodes as { id: string; data?: unknown }[], templateName) + const refId = resolveExtendsRef(nodes, templateName) if (visited.has(refId)) throw new Error(`Circular reference detected: ${templateName}`) const node = nodes.find((n) => n.id === refId && n.type === 'config') if (!node) throw new Error(`Config not found: ${templateName}`) - if (refId !== srcId && !isReachable(edges as { source: string; target: string }[], refId, id)) + if (refId !== srcId && !isReachable(edges, refId, id)) throw new Error(`Referenced config not connected to renderer: ${templateName}`) visited.add(refId) configIdsUsed.add(refId) @@ -40,10 +37,10 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC const configLoader = { getSource: (name: string): { src: string; path: string } | null => { - const refId = resolveExtendsRef(nodes as { id: string; data?: unknown }[], name) + const refId = resolveExtendsRef(nodes, name) const node = nodes.find((n) => n.id === refId && n.type === 'config') if (!node) return null - if (refId !== srcId && !isReachable(edges as { source: string; target: string }[], refId, id)) + if (refId !== srcId && !isReachable(edges, refId, id)) throw new Error(`Referenced config not connected to renderer: ${name}`) return { src: getConfigContent((node.data ?? undefined) as Record | undefined), @@ -53,7 +50,7 @@ export async function getResolvedContentForConfig(context: SourceRenderingLogicC } const nunjucksContext = Object.create(null) as Record - const setVarInContext = (src: Node) => { + const setVarInContext = (src: NodeLike) => { const v = (src.data as Record)?.value const str = v === undefined || v === null ? '' : String(v) nunjucksContext[src.id] = diff --git a/frontend/src/components/nodes/render/useRenderingNodeState.ts b/frontend/src/components/nodes/render/useRenderingNodeState.ts index 8fe8aca..55401ea 100644 --- a/frontend/src/components/nodes/render/useRenderingNodeState.ts +++ b/frontend/src/components/nodes/render/useRenderingNodeState.ts @@ -20,7 +20,7 @@ import { processSvgDisplay, stripTemplateSyntax, } from '@/lib/graph/rendering' -import { buildSourceSignatures } from '@/lib/graph/renderingSignatures' +import { buildSourceSignatures, type NodeLike, type EdgeLike } from '@/lib/graph/renderingSignatures' import type { ConfigTypeId, SourceRenderingLogicContext } from '@/lib/graph/rendering' export type RenderingNodeData = { @@ -137,13 +137,7 @@ export function useRenderingNodeState( : '' const signatures = useMemo( - () => - buildSourceSignatures( - nodes as { id: string; type?: string; data?: unknown }[], - edges as { source: string; target: string }[], - id, - incomingIds - ), + () => buildSourceSignatures(nodes as NodeLike[], edges as EdgeLike[], id, incomingIds), [nodes, edges, id, incomingIds] ) const { diff --git a/frontend/src/lib/graph/configTypes.ts b/frontend/src/lib/graph/configTypes.ts index 16b864a..023200f 100644 --- a/frontend/src/lib/graph/configTypes.ts +++ b/frontend/src/lib/graph/configTypes.ts @@ -144,7 +144,27 @@ async function renderWireframe(content: string, options?: RenderOptions): Promis return svg } -export const CONFIG_TYPES: ConfigType[] = [ +/** Registry for config (output) types. Built-in types register in registerBuiltinConfigTypes(). */ +const configTypeRegistry = new Map() + +export function registerConfigType(type: ConfigType): void { + if (configTypeRegistry.has(type.id)) { + console.warn(`[configTypes] Overwriting config type: ${type.id}`) + } + configTypeRegistry.set(type.id, type) +} + +/** All registered config types (for type selector, etc.). */ +export function getConfigTypes(): ConfigType[] { + return Array.from(configTypeRegistry.values()) +} + +/** All registered config type ids. */ +export function getConfigTypeIds(): string[] { + return Array.from(configTypeRegistry.keys()) +} + +const BUILTIN_CONFIG_TYPES: ConfigType[] = [ { id: 'plantuml', label: 'Diagram', @@ -220,11 +240,17 @@ export const CONFIG_TYPES: ConfigType[] = [ }, ] -export const CONFIG_TYPE_IDS = CONFIG_TYPES.map((t) => t.id) export const DEFAULT_CONFIG_TYPE_ID: ConfigTypeId = 'plantuml' -export function getConfigType(id: ConfigTypeId): ConfigType { - const t = CONFIG_TYPES.find((c) => c.id === id) +/** Register built-in config types (plantuml, markdown, wireframe). Call once at app init. */ +export function registerBuiltinConfigTypes(): void { + for (const t of BUILTIN_CONFIG_TYPES) { + registerConfigType(t) + } +} + +export function getConfigType(id: string): ConfigType { + const t = configTypeRegistry.get(id) if (!t) throw new Error(`Unknown config type: ${id}`) return t } @@ -264,9 +290,9 @@ export function getConfigContent(data: Record | undefined): str return typeof content === 'string' ? content : '' } -/** Config type id from data (default plantuml). */ +/** Config type id from data (default plantuml). Uses registered ids. */ export function getConfigTypeId(data: Record | undefined): ConfigTypeId { if (!data || data.configType == null) return 'plantuml' - const id = data.configType - return CONFIG_TYPE_IDS.includes(id as ConfigTypeId) ? (id as ConfigTypeId) : 'plantuml' + const id = String(data.configType) + return configTypeRegistry.has(id) ? (id as ConfigTypeId) : 'plantuml' } diff --git a/frontend/src/lib/graph/rendering.ts b/frontend/src/lib/graph/rendering.ts index a32a3dd..9f8e397 100644 --- a/frontend/src/lib/graph/rendering.ts +++ b/frontend/src/lib/graph/rendering.ts @@ -22,7 +22,7 @@ * * ## Implementing a new output type * - Add a config type (or equivalent) fulfilling `IOutputTypeRenderer`. - * - Register in CONFIG_TYPES and use `getConfigType(id)` in the resolve step. + * - Call registerConfigType(type) (e.g. from registerBuiltinConfigTypes) and use getConfigType(id) in the resolve step. * * ## Shared helpers * - templateRefs.ts: isReachable, resolveExtendsRef, getTemplateRefs (Nunjucks extends/include/import). diff --git a/frontend/src/lib/graph/renderingSignatures.ts b/frontend/src/lib/graph/renderingSignatures.ts index 41ef7d1..3aad9a5 100644 --- a/frontend/src/lib/graph/renderingSignatures.ts +++ b/frontend/src/lib/graph/renderingSignatures.ts @@ -1,14 +1,13 @@ /** * Pure functions to build "connected node ids" and source signatures for the * rendering pipeline. Used by useRenderingNodeState so the hook stays thin and - * this logic is testable in isolation. + * this logic is testable in isolation. Uses NodeLike/EdgeLike from templateRefs. */ import { getConfigContent } from '@/lib/graph/rendering' -import { isReachable, resolveExtendsRef, getTemplateRefs } from '@/lib/graph/templateRefs' +import { isReachable, resolveExtendsRef, getTemplateRefs, type NodeLike, type EdgeLike } from '@/lib/graph/templateRefs' -export type NodeLike = { id: string; type?: string; data?: unknown } -export type EdgeLike = { source: string; target: string } +export type { NodeLike, EdgeLike } from '@/lib/graph/templateRefs' /** * Collects all node ids that affect the render: direct incoming nodes plus diff --git a/frontend/src/lib/graph/sourceRenderingLogic.ts b/frontend/src/lib/graph/sourceRenderingLogic.ts index 980ed1c..4b9c501 100644 --- a/frontend/src/lib/graph/sourceRenderingLogic.ts +++ b/frontend/src/lib/graph/sourceRenderingLogic.ts @@ -7,17 +7,18 @@ */ import type { ConfigTypeId } from './configTypes' +import type { NodeLike, EdgeLike } from './templateRefs' export type { OutputMenuDescriptor, OutputMenuItemDescriptor, OutputMenuAction } from './configTypes' /** Context passed into the resolve step (graph, source id, callbacks). See {@link IRenderingContext}. */ export type SourceRenderingLogicContext = { - nodes: { id: string; type?: string; data?: unknown }[] - edges: { id: string; source: string; target: string }[] + nodes: NodeLike[] + edges: EdgeLike[] sourceNodeId: string renderNodeId: string viewportWidth?: number viewportHeight?: number - setNodes?: (updater: (nodes: { id: string; type?: string; data?: unknown }[]) => { id: string; type?: string; data?: unknown }[]) => void + setNodes?: (updater: (nodes: NodeLike[]) => NodeLike[]) => void aiConnection?: unknown onStreamingStart?: () => void onStreamingChunk?: (chunk: string) => void diff --git a/frontend/src/lib/graph/templateRefs.ts b/frontend/src/lib/graph/templateRefs.ts index cb82126..966aac3 100644 --- a/frontend/src/lib/graph/templateRefs.ts +++ b/frontend/src/lib/graph/templateRefs.ts @@ -4,8 +4,11 @@ * so template parsing and "connected configs" stay in one place. */ +/** Minimal edge shape for graph helpers (reachability, path, signatures). */ export type EdgeLike = { source: string; target: string } -export type NodeLike = { id: string; data?: unknown } + +/** Minimal node shape for graph helpers (resolve refs, signatures). */ +export type NodeLike = { id: string; type?: string; data?: unknown } /** BFS: is target reachable from start following directed edges? */ export function isReachable( diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 7906496..2e3bb6b 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -4,6 +4,7 @@ import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom' import { Toaster } from 'sonner' import { ThemeProvider } from './lib/themeContext' import { registerBuiltinNodes } from './lib/graph/registerBuiltinNodes' +import { registerBuiltinConfigTypes } from './lib/graph/configTypes' import { KosmosPage } from './app/kosmos/KosmosPage' import { ProjectsPage } from './app/pleroma/PleromaPage' import { KeromaPage } from './app/keroma/KeromaPage' @@ -11,6 +12,7 @@ import { CanvasRoute } from './app/canvas/CanvasRoute' import './styles.css' import '@xyflow/react/dist/style.css' +registerBuiltinConfigTypes() registerBuiltinNodes() createRoot(document.getElementById('root')!).render(