diff --git a/frontend/docs/IMPROVEMENTS.md b/frontend/docs/IMPROVEMENTS.md index 7baa255..a6bc696 100644 --- a/frontend/docs/IMPROVEMENTS.md +++ b/frontend/docs/IMPROVEMENTS.md @@ -48,6 +48,20 @@ This doc summarizes recent improvements and suggested next steps for readability - **config/renderingLogic**: uses context nodes/edges directly (no casts); `setVarInContext(src: NodeLike)`. - **useRenderingNodeState**: casts to `NodeLike[]` / `EdgeLike[]` when calling `buildSourceSignatures` (types from renderingSignatures). +### 9. Graph state, connection state, and node display state + +- **lib/graph/state.ts**: Central state contracts and flow doc. + - **GraphState** – in-memory nodes + edges (useGraphStateWithHistory, persisted by useCanvasGraph). + - **ConnectionPathState** – type describing the slice that drives edge status and path animation (trigger/updating/paused/error sets). + - **NodeDisplayStatus** – `'initial' | 'loading' | 'success' | 'error'` for node UI; **getNodeDisplayStatus({ loading, error, hasContent })**. + - **StoredGraphState** – shape for save/load (version + nodes + edges). +- **FlowContext**: Module doc splits value into (1) Graph state, (2) Connection path state, (3) UI state. Same flat props, clearer sections. +- **useGraphStateWithHistory**: JSDoc explains history (past/future), setNodes vs setNodesSilent, setStateImmediate. +- **projectGraphStorage**: Uses **StoredGraphState** from state.ts; re-exports type. Doc references state flow. +- **useRenderingNodeState**: Returns **displayStatus** (for NodeStatusIndicator/empty/error UI) and **lifecycle** (updating/error/paused for useSyncConnectionStatus). Hook calls useSyncConnectionStatus(id, state.lifecycle). Type includes RenderingNodeLifecycle. +- **RenderingNode**: Uses **state.displayStatus** for NodeStatusIndicator instead of computing status locally. +- **nodeLifecycle** and **connectionStatus**: Docs reference state.ts for overall state flow. + ## Design patterns in use | Pattern | Where | diff --git a/frontend/src/app/pleroma/projectGraphStorage.ts b/frontend/src/app/pleroma/projectGraphStorage.ts index 90b1b10..9967772 100644 --- a/frontend/src/app/pleroma/projectGraphStorage.ts +++ b/frontend/src/app/pleroma/projectGraphStorage.ts @@ -1,8 +1,11 @@ /** * Per-project graph persistence (localStorage). - * Used by CanvasPage to load/save and by ProjectsTablePage for export. + * Saves and loads StoredGraphState (version + nodes + edges). Used by useCanvasGraph and export. */ +import type { StoredGraphState } from '@/lib/graph/state' + +export type { StoredGraphState } export const PROJECT_FILE_EXT = '.zui.json' export const PROJECT_VERSION = 1 @@ -12,22 +15,20 @@ export function getGraphStorageKey(projectId: string): string { return `${GRAPH_KEY_PREFIX}${projectId}` } -export type GraphState = { version: number; nodes: unknown[]; edges: unknown[] } - -export function loadGraphFromStorage(projectId: string): GraphState | null { +export function loadGraphFromStorage(projectId: string): StoredGraphState | null { try { const raw = localStorage.getItem(getGraphStorageKey(projectId)) if (!raw) return null const data = JSON.parse(raw) as unknown - if (!data || typeof data !== 'object' || !Array.isArray((data as GraphState).nodes) || !Array.isArray((data as GraphState).edges)) + if (!data || typeof data !== 'object' || !Array.isArray((data as StoredGraphState).nodes) || !Array.isArray((data as StoredGraphState).edges)) return null - return data as GraphState + return data as StoredGraphState } catch { return null } } -export function saveGraphToStorage(projectId: string, state: GraphState): void { +export function saveGraphToStorage(projectId: string, state: StoredGraphState): void { localStorage.setItem(getGraphStorageKey(projectId), JSON.stringify(state)) } diff --git a/frontend/src/components/nodes/render/RenderingNode.tsx b/frontend/src/components/nodes/render/RenderingNode.tsx index fded9ab..7206e43 100644 --- a/frontend/src/components/nodes/render/RenderingNode.tsx +++ b/frontend/src/components/nodes/render/RenderingNode.tsx @@ -62,14 +62,6 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { ? { width, height } : undefined - const status = state.loading - ? 'loading' - : state.error - ? 'error' - : state.renderedContent - ? 'success' - : 'initial' - const { theme } = useTheme() const [rawEditorHeight, rawEditorContainerRef] = useResizeHeight(180, [viewMode]) @@ -138,7 +130,7 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) { } return ( - + 0 && sourceSignature !== lastRunSourceSignature - useSyncConnectionStatus(id, { updating: loading, error: error != null, paused: hasPendingInputs }) + const lifecycle = useMemo( + () => ({ updating: loading, error: error != null, paused: hasPendingInputs }), + [loading, error, hasPendingInputs] + ) + useSyncConnectionStatus(id, lifecycle) useEffect(() => { if (incomingIds.length === 0) { @@ -472,7 +489,14 @@ export function useRenderingNodeState( const sourceData = useMemo(() => (srcNode?.data as Record) ?? {}, [srcNode?.data]) const sourceNodeType = (srcNode?.type as string) ?? null + const displayStatus = useMemo( + () => getNodeDisplayStatus({ loading, error, hasContent: !!renderedContent }), + [loading, error, renderedContent] + ) + return { + displayStatus, + lifecycle, incomingIds, effectiveUpdateMode, runTrigger, diff --git a/frontend/src/hooks/useGraphStateWithHistory.ts b/frontend/src/hooks/useGraphStateWithHistory.ts index 6f1c3aa..c6b8c82 100644 --- a/frontend/src/hooks/useGraphStateWithHistory.ts +++ b/frontend/src/hooks/useGraphStateWithHistory.ts @@ -1,6 +1,11 @@ import { useCallback, useRef, useState } from 'react' import type { AppNode, AppEdge } from '@/lib/graph/nodeTypes' +/** + * In-memory graph state. History is a past/future stack of GraphState snapshots. + * setNodes/setEdges push current state to past; setNodesSilent/setEdgesSilent update without history. + * setStateImmediate replaces state and clears history (e.g. load example, import). + */ export type GraphState = { nodes: AppNode[]; edges: AppEdge[] } function cloneState(state: GraphState): GraphState { diff --git a/frontend/src/lib/graph/connectionStatus.ts b/frontend/src/lib/graph/connectionStatus.ts index 1517fc9..d5555d4 100644 --- a/frontend/src/lib/graph/connectionStatus.ts +++ b/frontend/src/lib/graph/connectionStatus.ts @@ -2,9 +2,8 @@ * Connection status: visual state of an edge (color/class). * Priority when multiple apply: error > paused > updating > default. * - * Status is derived from node lifecycle state: nodes report updating / paused / error - * via useSyncConnectionStatus() in nodeLifecycle.ts, which updates FlowContext sets. - * Edges read those sets here to pick the single status per edge. + * State flow: nodes report lifecycle via useSyncConnectionStatus() (nodeLifecycle.ts) → FlowContext + * holds the sets → edges pass those sets into getConnectionStatus() here. See lib/graph/state.ts. */ export type ConnectionStatus = 'default' | 'updating' | 'paused' | 'error' diff --git a/frontend/src/lib/graph/flowContext.tsx b/frontend/src/lib/graph/flowContext.tsx index 896925b..3ced060 100644 --- a/frontend/src/lib/graph/flowContext.tsx +++ b/frontend/src/lib/graph/flowContext.tsx @@ -1,3 +1,20 @@ +/** + * FlowContext provides graph state, connection path state, and UI state to nodes and edges. + * + * ## 1. Graph state (nodes, edges) + * - nodes, setNodes, edges, setEdges – owned by useCanvasGraph (history + persistence). + * + * ## 2. Connection path state (edge visuals and path animation) + * - Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus() in nodeLifecycle. + * - Context holds: connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds, connectionPathNodeIds, + * connectionPathPausedSegmentNodeIds, connectionPathActiveSegmentNodeIds, connectionPathPausedNodeIds, + * connectionPathErrorNodeIds, plus add/remove/start/end callbacks. + * - Edges use these in getConnectionStatus() for color (default / updating / paused / error). See connectionStatus.ts. + * + * ## 3. UI state + * - renamingNodeId, fullscreenNodeId, connectionFrom (drag-from handle), flowActionsRef, isValidConnection. + */ + import React, { useMemo } from 'react' import type { Connection } from '@xyflow/react' import type { AppNode, AppEdge } from './nodeTypes' @@ -13,48 +30,36 @@ export type FlowActions = { } export type FlowContextValue = { + // ---- Graph state ---- nodes: AppNode[] setNodes: (updater: AppNode[] | ((prev: AppNode[]) => AppNode[])) => void edges: AppEdge[] setEdges: (updater: AppEdge[] | ((prev: AppEdge[]) => AppEdge[])) => void + + // ---- UI state ---- 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 - /** 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. */ + + // ---- Connection path state (edge status and path animation; see state.ts ConnectionPathState) ---- 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 - /** Path nodes in the "paused" segment (from trigger up to Archon on hold). Those edges are yellow. */ connectionPathPausedSegmentNodeIds: Set - /** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */ connectionPathActiveSegmentNodeIds: Set - /** Nodes that are on hold (e.g. Renderer in manual mode 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 } diff --git a/frontend/src/lib/graph/nodeLifecycle.ts b/frontend/src/lib/graph/nodeLifecycle.ts index aecbcbd..d7315d8 100644 --- a/frontend/src/lib/graph/nodeLifecycle.ts +++ b/frontend/src/lib/graph/nodeLifecycle.ts @@ -2,30 +2,21 @@ * Node lifecycle: contract that nodes implement so the graph can show the right * connection status (edge colors) and path animation. * + * ## State flow + * + * Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus(id, state). + * This hook updates FlowContext sets; edges read them via getConnectionStatus() in connectionStatus.ts. + * See lib/graph/state.ts for the overall state flow (graph state, connection path state). + * * ## 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. renderer loading, agent run triggered by renderer). - * 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. renderer in manual mode 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). + * - **Idle** – Node is not on an active path; edges use default style. + * - **Trigger** – Node's output changed; path is computed from triggers + updating nodes. + * - **Updating** – Node is doing async work. Report `updating: true` → false. Path edges show blue. + * - **Paused** – Node is on hold (e.g. manual mode waiting for Run). Report `paused: true` → false. Segment shows yellow. + * - **Error** – Node has an error. Report `error: true` → false. Incoming edges show 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. + * Priority for edge status: error > paused > updating > default. */ import { useContext, useEffect, useRef } from 'react' diff --git a/frontend/src/lib/graph/state.ts b/frontend/src/lib/graph/state.ts new file mode 100644 index 0000000..0c3c6f5 --- /dev/null +++ b/frontend/src/lib/graph/state.ts @@ -0,0 +1,87 @@ +/** + * Central state contracts for the graph: in-memory graph, connection path (edge visuals), + * and node display status. Use these types to keep state flow predictable and readable. + * + * ## State flow (high level) + * + * 1. **Graph state** (nodes, edges) – Owned by useGraphStateWithHistory, persisted by useCanvasGraph. + * Updates via setNodes/setEdges (with undo) or setNodesSilent/setEdgesSilent (no history). + * + * 2. **Connection path state** – Owned by useCanvasConnectionPath, provided via FlowContext. + * Nodes report lifecycle (updating / error / paused) via useSyncConnectionStatus; the hook + * updates context sets. Edges read those sets and getConnectionStatus() to decide color (default / updating / paused / error). + * + * 3. **Node display state** – Each node type derives its own UI state (e.g. loading, success, error, initial). + * Rendering node uses displayStatus from useRenderingNodeState; other nodes can use getNodeDisplayStatus(). + */ + +import type { AppNode, AppEdge } from './nodeTypes' + +// --------------------------------------------------------------------------- +// Graph state (in-memory) +// --------------------------------------------------------------------------- + +/** In-memory graph: nodes and edges. Updated via setNodes/setEdges; history in useGraphStateWithHistory. */ +export type GraphState = { + nodes: AppNode[] + edges: AppEdge[] +} + +// --------------------------------------------------------------------------- +// Connection path state (edge visuals and path animation) +// --------------------------------------------------------------------------- + +/** + * Slice of FlowContext that drives edge status and path animation. + * Nodes report updating/error/paused via useSyncConnectionStatus; this state is derived from those reports. + * Edges use pathNodeIds, pausedSegmentNodeIds, activeSegmentNodeIds, errorTargetNodeIds in getConnectionStatus(). + */ +export type ConnectionPathState = { + /** Node ids currently doing async work (e.g. render loading). Path edges show "updating" (blue). */ + connectionPathUpdatingNodeIds: string[] + /** Node ids that triggered the update (e.g. config changed). Path = downstream(trigger) ∩ upstream(updating). */ + connectionPathTriggerNodeIds: string[] + /** All node ids on the path. Edges with both endpoints here animate. */ + connectionPathNodeIds: Set + /** Path nodes in the paused segment (yellow edges). */ + connectionPathPausedSegmentNodeIds: Set + /** Path nodes in the active segment (blue edges). */ + connectionPathActiveSegmentNodeIds: Set + /** Node ids on hold (e.g. render in manual mode). */ + connectionPathPausedNodeIds: string[] + /** Node ids with error (incoming edges show red). */ + connectionPathErrorNodeIds: string[] +} + +// --------------------------------------------------------------------------- +// Node display status (for UI: status indicator, empty state) +// --------------------------------------------------------------------------- + +/** Status for node UI: what to show in NodeStatusIndicator and empty/error states. */ +export type NodeDisplayStatus = 'initial' | 'loading' | 'success' | 'error' + +/** + * Derive display status from node state. Use in nodes to drive NodeStatusIndicator and empty/error UI. + * Priority: loading > error > hasContent (success) > initial. + */ +export function getNodeDisplayStatus(state: { + loading: boolean + error: unknown + hasContent: boolean +}): NodeDisplayStatus { + if (state.loading) return 'loading' + if (state.error != null) return 'error' + if (state.hasContent) return 'success' + return 'initial' +} + +// --------------------------------------------------------------------------- +// Stored graph state (persistence) +// --------------------------------------------------------------------------- + +/** Shape of graph state when saved/loaded (e.g. localStorage). version for future migrations. */ +export type StoredGraphState = { + version: number + nodes: unknown[] + edges: unknown[] +}