@foresthubai/workflow-builder 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -661
- package/NOTICE +16 -16
- package/README.md +110 -93
- package/dist/components/ui/command.d.ts +2 -2
- package/dist/components/ui/input.d.ts +1 -1
- package/dist/components/ui/resizable.d.ts +1 -1
- package/dist/components/ui/textarea.d.ts +1 -1
- package/dist/graph/BaseNode.js +10 -10
- package/dist/graph/reactFlowRegistry.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/toolbars/CanvasTabsToolbar.d.ts +11 -0
- package/dist/toolbars/CanvasTabsToolbar.d.ts.map +1 -0
- package/dist/toolbars/CanvasTabsToolbar.js +101 -0
- package/dist/toolbars/CanvasTabsToolbar.js.map +1 -0
- package/package.json +2 -2
- package/src/BuilderLayout.tsx +345 -345
- package/src/Canvas.tsx +261 -261
- package/src/CanvasEditor.tsx +142 -142
- package/src/CanvasTabsToolbar.tsx +176 -176
- package/src/RightConfigPanel.tsx +266 -266
- package/src/WorkflowBuilder.tsx +412 -412
- package/src/cn.ts +6 -6
- package/src/components/ui/add-button.tsx +39 -39
- package/src/components/ui/alert-dialog.tsx +141 -141
- package/src/components/ui/alert.tsx +59 -59
- package/src/components/ui/badge.tsx +36 -36
- package/src/components/ui/button.tsx +85 -85
- package/src/components/ui/card.tsx +79 -79
- package/src/components/ui/checkbox.tsx +28 -28
- package/src/components/ui/collapsible.tsx +9 -9
- package/src/components/ui/command.tsx +153 -153
- package/src/components/ui/delete-button.tsx +23 -23
- package/src/components/ui/dialog.tsx +125 -125
- package/src/components/ui/dropdown-menu.tsx +198 -198
- package/src/components/ui/input.tsx +55 -55
- package/src/components/ui/label.tsx +24 -24
- package/src/components/ui/readonly-banner.tsx +15 -15
- package/src/components/ui/resizable.tsx +43 -43
- package/src/components/ui/scroll-area.tsx +102 -102
- package/src/components/ui/select.tsx +160 -160
- package/src/components/ui/separator.tsx +29 -29
- package/src/components/ui/switch.tsx +27 -27
- package/src/components/ui/textarea.tsx +51 -51
- package/src/components/ui/toast.tsx +127 -127
- package/src/components/ui/toaster.tsx +33 -33
- package/src/components/ui/toggle-group.tsx +59 -59
- package/src/components/ui/toggle.tsx +43 -43
- package/src/components/ui/tooltip.tsx +32 -32
- package/src/dialogs/NodePickerDialog.tsx +84 -84
- package/src/dialogs/ValidationDialog.tsx +184 -184
- package/src/graph/BaseNode.tsx +557 -557
- package/src/graph/CustomEdge.tsx +185 -185
- package/src/graph/CustomNode.tsx +16 -16
- package/src/graph/FunctionCallNode.tsx +30 -30
- package/src/graph/PortHandle.tsx +189 -189
- package/src/graph/reactFlowRegistry.ts +26 -26
- package/src/hooks/use-toast.ts +125 -125
- package/src/hooks/useAvailableVariables.ts +20 -20
- package/src/hooks/useCanvasHistory.ts +22 -22
- package/src/hooks/useCanvasTabs.ts +168 -168
- package/src/hooks/useFunctionDiagnosticsSync.ts +40 -40
- package/src/hooks/useFunctionRegistry.ts +26 -26
- package/src/hooks/useFunctions.ts +44 -44
- package/src/hooks/useGraph.ts +161 -161
- package/src/hooks/useNodeDefinitions.ts +82 -82
- package/src/hooks/useParamErrors.ts +26 -26
- package/src/hooks/useResolvedTheme.ts +30 -30
- package/src/hooks/useResourceDiagnosticsSync.ts +58 -58
- package/src/hooks/useSuppressThemeTransition.ts +79 -79
- package/src/hooks/useWorkflowSerialization.ts +127 -127
- package/src/i18n/index.ts +53 -53
- package/src/i18n/locales/de.json +501 -501
- package/src/i18n/locales/en.json +557 -557
- package/src/index.ts +27 -27
- package/src/inputs/ExpressionInput.tsx +297 -297
- package/src/inputs/ParameterEditor.tsx +515 -515
- package/src/inputs/PortSection.tsx +144 -144
- package/src/panels/BuilderSidebar.tsx +301 -301
- package/src/panels/ChannelConfigPanel.tsx +49 -49
- package/src/panels/ChannelsPanel.tsx +28 -28
- package/src/panels/DebugConsolePanel.tsx +73 -73
- package/src/panels/DebugContextPanel.tsx +77 -77
- package/src/panels/DebugExternalIOPanel.tsx +180 -180
- package/src/panels/DiagnosticsPanel.tsx +170 -170
- package/src/panels/EdgeConfigPanel.tsx +104 -104
- package/src/panels/FunctionConfigPanel.tsx +179 -179
- package/src/panels/FunctionListPanel.tsx +45 -45
- package/src/panels/MemoryConfigPanel.tsx +55 -55
- package/src/panels/MemoryPanel.tsx +40 -40
- package/src/panels/ModelConfigPanel.tsx +41 -41
- package/src/panels/ModelsPanel.tsx +36 -36
- package/src/panels/NodeConfigPanel.tsx +630 -630
- package/src/panels/NodeLibrary.tsx +288 -288
- package/src/panels/ResourceConfigPanel.tsx +132 -132
- package/src/panels/ResourceListPanel.tsx +113 -113
- package/src/panels/VariableConfigPanel.tsx +161 -161
- package/src/panels/VariablesPanel.tsx +145 -145
- package/src/stores/canvasStore.test.ts +44 -44
- package/src/stores/canvasStore.ts +245 -245
- package/src/stores/debugStore.ts +74 -74
- package/src/stores/diagnosticsStore.ts +130 -130
- package/src/stores/editorStore.ts +202 -202
- package/src/styles/index.css +526 -526
- package/src/utils/categoryConstants.ts +26 -26
- package/src/utils/channelOperations.ts +86 -86
- package/src/utils/connectionRules.ts +137 -137
- package/src/utils/functionOperations.ts +179 -179
- package/src/utils/graphOperations.ts +550 -550
- package/src/utils/history.ts +207 -207
- package/src/utils/memoryOperations.ts +57 -57
- package/src/utils/migrateFunctionNodes.ts +107 -107
- package/src/utils/modelOperations.ts +55 -55
- package/src/utils/paramDisplay.ts +71 -71
- package/src/utils/resourceHelpers.ts +32 -32
- package/src/utils/translation.ts +28 -28
- package/src/utils/variableOperations.ts +75 -75
- package/tailwind-preset.ts +166 -166
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import { useMemo, useCallback } from "react";
|
|
2
|
-
import i18n from "../i18n";
|
|
3
|
-
import { NodeCategory, NodeRegistry, NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
|
|
4
|
-
import { useFunctionRegistry } from "./useFunctionRegistry";
|
|
5
|
-
import { toFunctionInfo, type FunctionInfo } from "@foresthubai/workflow-core/function";
|
|
6
|
-
import { FunctionCallNode, FunctionNodeDefinition, buildFunctionNodeDef as coreBuildFunctionNodeDef } from "@foresthubai/workflow-core/node";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Workflow-builder binding for {@link coreBuildFunctionNodeDef} — passes
|
|
10
|
-
* `i18n.t` so descriptions are translated. Consumers continue to call
|
|
11
|
-
* `buildFunctionNodeDef(fn)` unchanged; core's signature is the pure
|
|
12
|
-
* `(fn, t?)` form.
|
|
13
|
-
*/
|
|
14
|
-
export function buildFunctionNodeDef(fn: FunctionInfo): FunctionNodeDefinition {
|
|
15
|
-
return coreBuildFunctionNodeDef(fn, i18n.t.bind(i18n));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Use function registry to provide dynamic node definitions based on available functions
|
|
19
|
-
export const useNodeDefinitions = () => {
|
|
20
|
-
// Get static node definitions from registry (these never change)
|
|
21
|
-
const staticNodeDefs: NodeDefinition[] = NodeRegistry.getAll();
|
|
22
|
-
|
|
23
|
-
// Subscribe to function registry (derived from all canvas stores)
|
|
24
|
-
const { functions } = useFunctionRegistry();
|
|
25
|
-
|
|
26
|
-
// Dynamically create node definitions for each function. The call-site node def is
|
|
27
|
-
// built from the flat signature snapshot, so project the domain declaration here.
|
|
28
|
-
const functionNodeDefs: FunctionNodeDefinition[] = useMemo(
|
|
29
|
-
() => Object.values(functions).map((fn) => buildFunctionNodeDef(toFunctionInfo(fn))),
|
|
30
|
-
[functions],
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Get node definition for a node instance (still depending on all functions)
|
|
34
|
-
const getNodeDefinition = useCallback(
|
|
35
|
-
(node: NodeData): NodeDefinition | undefined => {
|
|
36
|
-
if (node.type === "FunctionCall") {
|
|
37
|
-
const fnNode = node as FunctionCallNode;
|
|
38
|
-
return functionNodeDefs.find((def) => def.type === "FunctionCall" && def.functionInfo.id === fnNode.functionInfo.id);
|
|
39
|
-
}
|
|
40
|
-
return NodeRegistry.getByType(node.type);
|
|
41
|
-
},
|
|
42
|
-
[functionNodeDefs],
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const getNodeDefinitionsByCategory = useCallback(
|
|
46
|
-
(category: NodeCategory) => {
|
|
47
|
-
const staticNodes = NodeRegistry.getByCategory(category);
|
|
48
|
-
if (category === NodeCategory.Function) {
|
|
49
|
-
return [...staticNodes, ...functionNodeDefs];
|
|
50
|
-
}
|
|
51
|
-
return staticNodes;
|
|
52
|
-
},
|
|
53
|
-
[functionNodeDefs],
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const getAllCategories = useCallback((): NodeCategory[] => {
|
|
57
|
-
const staticCategories = NodeRegistry.getAllCategories();
|
|
58
|
-
const allCategories = new Set([...staticCategories]);
|
|
59
|
-
if (functionNodeDefs.length > 0) {
|
|
60
|
-
allCategories.add(NodeCategory.Function);
|
|
61
|
-
}
|
|
62
|
-
const categoryOrder = [
|
|
63
|
-
NodeCategory.Trigger,
|
|
64
|
-
NodeCategory.Input,
|
|
65
|
-
NodeCategory.Logic,
|
|
66
|
-
NodeCategory.Data,
|
|
67
|
-
NodeCategory.Function,
|
|
68
|
-
NodeCategory.AI,
|
|
69
|
-
NodeCategory.Tool,
|
|
70
|
-
NodeCategory.Output,
|
|
71
|
-
];
|
|
72
|
-
return categoryOrder.filter((cat) => allCategories.has(cat));
|
|
73
|
-
}, [functionNodeDefs]);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
nodeDefinitions: [...staticNodeDefs, ...functionNodeDefs],
|
|
77
|
-
getAllCategories,
|
|
78
|
-
getNodeDefinition,
|
|
79
|
-
getNodeDefinitionsByCategory,
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
|
|
1
|
+
import { useMemo, useCallback } from "react";
|
|
2
|
+
import i18n from "../i18n";
|
|
3
|
+
import { NodeCategory, NodeRegistry, NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
|
|
4
|
+
import { useFunctionRegistry } from "./useFunctionRegistry";
|
|
5
|
+
import { toFunctionInfo, type FunctionInfo } from "@foresthubai/workflow-core/function";
|
|
6
|
+
import { FunctionCallNode, FunctionNodeDefinition, buildFunctionNodeDef as coreBuildFunctionNodeDef } from "@foresthubai/workflow-core/node";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Workflow-builder binding for {@link coreBuildFunctionNodeDef} — passes
|
|
10
|
+
* `i18n.t` so descriptions are translated. Consumers continue to call
|
|
11
|
+
* `buildFunctionNodeDef(fn)` unchanged; core's signature is the pure
|
|
12
|
+
* `(fn, t?)` form.
|
|
13
|
+
*/
|
|
14
|
+
export function buildFunctionNodeDef(fn: FunctionInfo): FunctionNodeDefinition {
|
|
15
|
+
return coreBuildFunctionNodeDef(fn, i18n.t.bind(i18n));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Use function registry to provide dynamic node definitions based on available functions
|
|
19
|
+
export const useNodeDefinitions = () => {
|
|
20
|
+
// Get static node definitions from registry (these never change)
|
|
21
|
+
const staticNodeDefs: NodeDefinition[] = NodeRegistry.getAll();
|
|
22
|
+
|
|
23
|
+
// Subscribe to function registry (derived from all canvas stores)
|
|
24
|
+
const { functions } = useFunctionRegistry();
|
|
25
|
+
|
|
26
|
+
// Dynamically create node definitions for each function. The call-site node def is
|
|
27
|
+
// built from the flat signature snapshot, so project the domain declaration here.
|
|
28
|
+
const functionNodeDefs: FunctionNodeDefinition[] = useMemo(
|
|
29
|
+
() => Object.values(functions).map((fn) => buildFunctionNodeDef(toFunctionInfo(fn))),
|
|
30
|
+
[functions],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Get node definition for a node instance (still depending on all functions)
|
|
34
|
+
const getNodeDefinition = useCallback(
|
|
35
|
+
(node: NodeData): NodeDefinition | undefined => {
|
|
36
|
+
if (node.type === "FunctionCall") {
|
|
37
|
+
const fnNode = node as FunctionCallNode;
|
|
38
|
+
return functionNodeDefs.find((def) => def.type === "FunctionCall" && def.functionInfo.id === fnNode.functionInfo.id);
|
|
39
|
+
}
|
|
40
|
+
return NodeRegistry.getByType(node.type);
|
|
41
|
+
},
|
|
42
|
+
[functionNodeDefs],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const getNodeDefinitionsByCategory = useCallback(
|
|
46
|
+
(category: NodeCategory) => {
|
|
47
|
+
const staticNodes = NodeRegistry.getByCategory(category);
|
|
48
|
+
if (category === NodeCategory.Function) {
|
|
49
|
+
return [...staticNodes, ...functionNodeDefs];
|
|
50
|
+
}
|
|
51
|
+
return staticNodes;
|
|
52
|
+
},
|
|
53
|
+
[functionNodeDefs],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const getAllCategories = useCallback((): NodeCategory[] => {
|
|
57
|
+
const staticCategories = NodeRegistry.getAllCategories();
|
|
58
|
+
const allCategories = new Set([...staticCategories]);
|
|
59
|
+
if (functionNodeDefs.length > 0) {
|
|
60
|
+
allCategories.add(NodeCategory.Function);
|
|
61
|
+
}
|
|
62
|
+
const categoryOrder = [
|
|
63
|
+
NodeCategory.Trigger,
|
|
64
|
+
NodeCategory.Input,
|
|
65
|
+
NodeCategory.Logic,
|
|
66
|
+
NodeCategory.Data,
|
|
67
|
+
NodeCategory.Function,
|
|
68
|
+
NodeCategory.AI,
|
|
69
|
+
NodeCategory.Tool,
|
|
70
|
+
NodeCategory.Output,
|
|
71
|
+
];
|
|
72
|
+
return categoryOrder.filter((cat) => allCategories.has(cat));
|
|
73
|
+
}, [functionNodeDefs]);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
nodeDefinitions: [...staticNodeDefs, ...functionNodeDefs],
|
|
77
|
+
getAllCategories,
|
|
78
|
+
getNodeDefinition,
|
|
79
|
+
getNodeDefinitionsByCategory,
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Build a per-parameter error map (`paramId → messages`) from a resource's
|
|
6
|
-
* diagnostics, keeping only `error`-severity entries. Shared by every config
|
|
7
|
-
* panel that renders a parameter list (node / edge / channel / memory / model)
|
|
8
|
-
* so the inline Map-building loop lives in exactly one place.
|
|
9
|
-
*
|
|
10
|
-
* Memoized on `diags`, so callers can read it from the diagnostics store with a
|
|
11
|
-
* plain selector and pass the result straight through to `<ParameterEditor>`.
|
|
12
|
-
*/
|
|
13
|
-
export function useParamErrors(diags: Diagnostic[] | undefined): Map<string, string[]> {
|
|
14
|
-
return useMemo(() => {
|
|
15
|
-
const map = new Map<string, string[]>();
|
|
16
|
-
if (!diags) return map;
|
|
17
|
-
for (const d of diags) {
|
|
18
|
-
if (d.paramId && d.severity === "error") {
|
|
19
|
-
const arr = map.get(d.paramId);
|
|
20
|
-
if (arr) arr.push(d.message);
|
|
21
|
-
else map.set(d.paramId, [d.message]);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return map;
|
|
25
|
-
}, [diags]);
|
|
26
|
-
}
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a per-parameter error map (`paramId → messages`) from a resource's
|
|
6
|
+
* diagnostics, keeping only `error`-severity entries. Shared by every config
|
|
7
|
+
* panel that renders a parameter list (node / edge / channel / memory / model)
|
|
8
|
+
* so the inline Map-building loop lives in exactly one place.
|
|
9
|
+
*
|
|
10
|
+
* Memoized on `diags`, so callers can read it from the diagnostics store with a
|
|
11
|
+
* plain selector and pass the result straight through to `<ParameterEditor>`.
|
|
12
|
+
*/
|
|
13
|
+
export function useParamErrors(diags: Diagnostic[] | undefined): Map<string, string[]> {
|
|
14
|
+
return useMemo(() => {
|
|
15
|
+
const map = new Map<string, string[]>();
|
|
16
|
+
if (!diags) return map;
|
|
17
|
+
for (const d of diags) {
|
|
18
|
+
if (d.paramId && d.severity === "error") {
|
|
19
|
+
const arr = map.get(d.paramId);
|
|
20
|
+
if (arr) arr.push(d.message);
|
|
21
|
+
else map.set(d.paramId, [d.message]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}, [diags]);
|
|
26
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Returns "light" if `<html>` has the `light` class, else "dark". Subscribes to
|
|
5
|
-
* MutationObserver so toggles by the embedder propagate immediately.
|
|
6
|
-
*
|
|
7
|
-
* Dark is the default — the builder's CSS puts dark tokens on `:root` and
|
|
8
|
-
* overrides them under `.light`. This hook exists so things that need an
|
|
9
|
-
* explicit value — notably ReactFlow's `colorMode` prop — stay in sync.
|
|
10
|
-
*/
|
|
11
|
-
export function useResolvedTheme(): "dark" | "light" {
|
|
12
|
-
const [theme, setTheme] = useState<"dark" | "light">(() => detect());
|
|
13
|
-
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (typeof document === "undefined") return;
|
|
16
|
-
const root = document.documentElement;
|
|
17
|
-
const observer = new MutationObserver(() => setTheme(detect()));
|
|
18
|
-
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
19
|
-
// Initial sync in case it changed between mount and effect.
|
|
20
|
-
setTheme(detect());
|
|
21
|
-
return () => observer.disconnect();
|
|
22
|
-
}, []);
|
|
23
|
-
|
|
24
|
-
return theme;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function detect(): "dark" | "light" {
|
|
28
|
-
if (typeof document === "undefined") return "dark";
|
|
29
|
-
return document.documentElement.classList.contains("light") ? "light" : "dark";
|
|
30
|
-
}
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns "light" if `<html>` has the `light` class, else "dark". Subscribes to
|
|
5
|
+
* MutationObserver so toggles by the embedder propagate immediately.
|
|
6
|
+
*
|
|
7
|
+
* Dark is the default — the builder's CSS puts dark tokens on `:root` and
|
|
8
|
+
* overrides them under `.light`. This hook exists so things that need an
|
|
9
|
+
* explicit value — notably ReactFlow's `colorMode` prop — stay in sync.
|
|
10
|
+
*/
|
|
11
|
+
export function useResolvedTheme(): "dark" | "light" {
|
|
12
|
+
const [theme, setTheme] = useState<"dark" | "light">(() => detect());
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (typeof document === "undefined") return;
|
|
16
|
+
const root = document.documentElement;
|
|
17
|
+
const observer = new MutationObserver(() => setTheme(detect()));
|
|
18
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
19
|
+
// Initial sync in case it changed between mount and effect.
|
|
20
|
+
setTheme(detect());
|
|
21
|
+
return () => observer.disconnect();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return theme;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detect(): "dark" | "light" {
|
|
28
|
+
if (typeof document === "undefined") return "dark";
|
|
29
|
+
return document.documentElement.classList.contains("light") ? "light" : "dark";
|
|
30
|
+
}
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import { useEditorStore } from "../stores/editorStore";
|
|
3
|
-
import { useDiagnosticsStore } from "../stores/diagnosticsStore";
|
|
4
|
-
import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
|
|
5
|
-
|
|
6
|
-
type EditorState = ReturnType<typeof useEditorStore.getState>;
|
|
7
|
-
type DiagnosticsState = ReturnType<typeof useDiagnosticsStore.getState>;
|
|
8
|
-
|
|
9
|
-
interface ResourceDiagnosticsSyncConfig<I extends { id: string }> {
|
|
10
|
-
/** Pick the resource map (e.g. `s.channels`) off the editor store. */
|
|
11
|
-
selectItems: (s: EditorState) => Record<string, I>;
|
|
12
|
-
/** Validate one instance into its diagnostics. */
|
|
13
|
-
validate: (item: I) => Diagnostic[];
|
|
14
|
-
/** Read the matching diagnostics slot (e.g. `d.byChannelId`). */
|
|
15
|
-
getStored: (d: DiagnosticsState) => Record<string, Diagnostic[]>;
|
|
16
|
-
/** Write one instance's diagnostics. */
|
|
17
|
-
set: (d: DiagnosticsState, id: string, diags: Diagnostic[]) => void;
|
|
18
|
-
/** Drop one instance's diagnostics. */
|
|
19
|
-
clear: (d: DiagnosticsState, id: string) => void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Keeps a project-scoped diagnostics slot (`byChannelId` / `byMemoryId` /
|
|
24
|
-
* `byModelId`) in sync with the editor's resource map.
|
|
25
|
-
*
|
|
26
|
-
* These resources are project-scoped, not canvas-scoped, and are only rendered
|
|
27
|
-
* visually when their sidebar tab is open. Tying diagnostic writes to card
|
|
28
|
-
* lifecycles would mean errors vanish the moment that tab closes, so this hook
|
|
29
|
-
* is mounted once at the workflow-builder root and reactively rewrites the store
|
|
30
|
-
* whenever the resource map changes.
|
|
31
|
-
*
|
|
32
|
-
* Lifecycle handled implicitly by the effect:
|
|
33
|
-
* - Load → setItems fires → effect re-runs → diagnostics written
|
|
34
|
-
* - Edit → store mutates → effect re-runs → entry replaced
|
|
35
|
-
* - Delete → item leaves → orphan branch → entry cleared
|
|
36
|
-
* - Unmount → store goes down with the app (no cleanup needed)
|
|
37
|
-
*/
|
|
38
|
-
export function useResourceDiagnosticsSync<I extends { id: string }>(config: ResourceDiagnosticsSyncConfig<I>): void {
|
|
39
|
-
const items = useEditorStore(config.selectItems);
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
const ds = useDiagnosticsStore.getState();
|
|
43
|
-
|
|
44
|
-
const seen = new Set<string>();
|
|
45
|
-
for (const item of Object.values(items)) {
|
|
46
|
-
seen.add(item.id);
|
|
47
|
-
config.set(ds, item.id, config.validate(item));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Drop entries for items that have been deleted.
|
|
51
|
-
for (const id of Object.keys(config.getStored(ds))) {
|
|
52
|
-
if (!seen.has(id)) config.clear(ds, id);
|
|
53
|
-
}
|
|
54
|
-
// `config` is recreated each render but only `items` drives a resync; the
|
|
55
|
-
// effect reads the latest closure on every run.
|
|
56
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
-
}, [items]);
|
|
58
|
-
}
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useEditorStore } from "../stores/editorStore";
|
|
3
|
+
import { useDiagnosticsStore } from "../stores/diagnosticsStore";
|
|
4
|
+
import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
|
|
5
|
+
|
|
6
|
+
type EditorState = ReturnType<typeof useEditorStore.getState>;
|
|
7
|
+
type DiagnosticsState = ReturnType<typeof useDiagnosticsStore.getState>;
|
|
8
|
+
|
|
9
|
+
interface ResourceDiagnosticsSyncConfig<I extends { id: string }> {
|
|
10
|
+
/** Pick the resource map (e.g. `s.channels`) off the editor store. */
|
|
11
|
+
selectItems: (s: EditorState) => Record<string, I>;
|
|
12
|
+
/** Validate one instance into its diagnostics. */
|
|
13
|
+
validate: (item: I) => Diagnostic[];
|
|
14
|
+
/** Read the matching diagnostics slot (e.g. `d.byChannelId`). */
|
|
15
|
+
getStored: (d: DiagnosticsState) => Record<string, Diagnostic[]>;
|
|
16
|
+
/** Write one instance's diagnostics. */
|
|
17
|
+
set: (d: DiagnosticsState, id: string, diags: Diagnostic[]) => void;
|
|
18
|
+
/** Drop one instance's diagnostics. */
|
|
19
|
+
clear: (d: DiagnosticsState, id: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Keeps a project-scoped diagnostics slot (`byChannelId` / `byMemoryId` /
|
|
24
|
+
* `byModelId`) in sync with the editor's resource map.
|
|
25
|
+
*
|
|
26
|
+
* These resources are project-scoped, not canvas-scoped, and are only rendered
|
|
27
|
+
* visually when their sidebar tab is open. Tying diagnostic writes to card
|
|
28
|
+
* lifecycles would mean errors vanish the moment that tab closes, so this hook
|
|
29
|
+
* is mounted once at the workflow-builder root and reactively rewrites the store
|
|
30
|
+
* whenever the resource map changes.
|
|
31
|
+
*
|
|
32
|
+
* Lifecycle handled implicitly by the effect:
|
|
33
|
+
* - Load → setItems fires → effect re-runs → diagnostics written
|
|
34
|
+
* - Edit → store mutates → effect re-runs → entry replaced
|
|
35
|
+
* - Delete → item leaves → orphan branch → entry cleared
|
|
36
|
+
* - Unmount → store goes down with the app (no cleanup needed)
|
|
37
|
+
*/
|
|
38
|
+
export function useResourceDiagnosticsSync<I extends { id: string }>(config: ResourceDiagnosticsSyncConfig<I>): void {
|
|
39
|
+
const items = useEditorStore(config.selectItems);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const ds = useDiagnosticsStore.getState();
|
|
43
|
+
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
for (const item of Object.values(items)) {
|
|
46
|
+
seen.add(item.id);
|
|
47
|
+
config.set(ds, item.id, config.validate(item));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Drop entries for items that have been deleted.
|
|
51
|
+
for (const id of Object.keys(config.getStored(ds))) {
|
|
52
|
+
if (!seen.has(id)) config.clear(ds, id);
|
|
53
|
+
}
|
|
54
|
+
// `config` is recreated each render but only `items` drives a resync; the
|
|
55
|
+
// effect reads the latest closure on every run.
|
|
56
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
57
|
+
}, [items]);
|
|
58
|
+
}
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect } from "react";
|
|
2
|
-
|
|
3
|
-
import { useResolvedTheme } from "./useResolvedTheme";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Makes color-mode switches snap instead of fade.
|
|
7
|
-
*
|
|
8
|
-
* The embedder toggles the `light` class on `<html>`. Two different mechanisms
|
|
9
|
-
* recolor the UI when it does, and both would otherwise animate to the new
|
|
10
|
-
* tokens over their transition duration:
|
|
11
|
-
*
|
|
12
|
-
* 1. CSS-variable cascade — components with `transition-all` (canvas tabs, the
|
|
13
|
-
* builder sidebar). These recolor the instant the class flips.
|
|
14
|
-
* 2. React re-render — ReactFlow's controls and node chrome recolor only once
|
|
15
|
-
* `useResolvedTheme` re-renders and ReactFlow re-applies its `colorMode`
|
|
16
|
-
* class, which commits a tick or two *after* the class flip.
|
|
17
|
-
*
|
|
18
|
-
* So a fixed one-frame suppression window catches (1) but misses (2). Instead:
|
|
19
|
-
*
|
|
20
|
-
* - A MutationObserver adds `theme-changing` (CSS kills all transitions under
|
|
21
|
-
* it) the moment the class flips — a microtask, before any paint — and forces
|
|
22
|
-
* a reflow so the cascade-driven colors commit with no transition.
|
|
23
|
-
* - A layout effect keyed on the resolved theme removes it. Parent layout
|
|
24
|
-
* effects run after children's, so this fires after ReactFlow has committed
|
|
25
|
-
* its `colorMode` change; a reflow first flushes those colors while
|
|
26
|
-
* transitions are still suppressed, then we restore them. Tying removal to
|
|
27
|
-
* the React commit (not a timer) makes it deterministic regardless of how
|
|
28
|
-
* late the re-render lands.
|
|
29
|
-
*
|
|
30
|
-
* A short fallback timer removes the class even if no re-render follows, so a
|
|
31
|
-
* theme-only-affects-CSS flip can never leave transitions permanently off.
|
|
32
|
-
*
|
|
33
|
-
* Mount once at the builder root.
|
|
34
|
-
*/
|
|
35
|
-
export function useSuppressThemeTransition(): void {
|
|
36
|
-
// Re-renders on every color-mode flip; drives the layout effect below.
|
|
37
|
-
const theme = useResolvedTheme();
|
|
38
|
-
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (typeof document === "undefined") return;
|
|
41
|
-
const root = document.documentElement;
|
|
42
|
-
|
|
43
|
-
let wasLight = root.classList.contains("light");
|
|
44
|
-
let fallback = 0;
|
|
45
|
-
|
|
46
|
-
const observer = new MutationObserver(() => {
|
|
47
|
-
const isLight = root.classList.contains("light");
|
|
48
|
-
if (isLight === wasLight) return; // class changed for some other reason
|
|
49
|
-
wasLight = isLight;
|
|
50
|
-
|
|
51
|
-
root.classList.add("theme-changing");
|
|
52
|
-
// Force a synchronous reflow so cascade-driven colors commit while
|
|
53
|
-
// transitions are disabled, before the browser's next paint.
|
|
54
|
-
void root.offsetHeight;
|
|
55
|
-
|
|
56
|
-
// Safety net: if no React re-render follows (theme flip touched only CSS),
|
|
57
|
-
// the layout effect won't run — drop the class anyway after a beat.
|
|
58
|
-
clearTimeout(fallback);
|
|
59
|
-
fallback = window.setTimeout(() => root.classList.remove("theme-changing"), 120);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
63
|
-
return () => {
|
|
64
|
-
observer.disconnect();
|
|
65
|
-
clearTimeout(fallback);
|
|
66
|
-
root.classList.remove("theme-changing");
|
|
67
|
-
};
|
|
68
|
-
}, []);
|
|
69
|
-
|
|
70
|
-
useLayoutEffect(() => {
|
|
71
|
-
if (typeof document === "undefined") return;
|
|
72
|
-
const root = document.documentElement;
|
|
73
|
-
if (!root.classList.contains("theme-changing")) return; // initial mount, no flip
|
|
74
|
-
// ReactFlow has committed its colorMode change by now (child layout effects
|
|
75
|
-
// run first). Flush those colors under suppression, then restore transitions.
|
|
76
|
-
void root.offsetHeight;
|
|
77
|
-
root.classList.remove("theme-changing");
|
|
78
|
-
}, [theme]);
|
|
79
|
-
}
|
|
1
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import { useResolvedTheme } from "./useResolvedTheme";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Makes color-mode switches snap instead of fade.
|
|
7
|
+
*
|
|
8
|
+
* The embedder toggles the `light` class on `<html>`. Two different mechanisms
|
|
9
|
+
* recolor the UI when it does, and both would otherwise animate to the new
|
|
10
|
+
* tokens over their transition duration:
|
|
11
|
+
*
|
|
12
|
+
* 1. CSS-variable cascade — components with `transition-all` (canvas tabs, the
|
|
13
|
+
* builder sidebar). These recolor the instant the class flips.
|
|
14
|
+
* 2. React re-render — ReactFlow's controls and node chrome recolor only once
|
|
15
|
+
* `useResolvedTheme` re-renders and ReactFlow re-applies its `colorMode`
|
|
16
|
+
* class, which commits a tick or two *after* the class flip.
|
|
17
|
+
*
|
|
18
|
+
* So a fixed one-frame suppression window catches (1) but misses (2). Instead:
|
|
19
|
+
*
|
|
20
|
+
* - A MutationObserver adds `theme-changing` (CSS kills all transitions under
|
|
21
|
+
* it) the moment the class flips — a microtask, before any paint — and forces
|
|
22
|
+
* a reflow so the cascade-driven colors commit with no transition.
|
|
23
|
+
* - A layout effect keyed on the resolved theme removes it. Parent layout
|
|
24
|
+
* effects run after children's, so this fires after ReactFlow has committed
|
|
25
|
+
* its `colorMode` change; a reflow first flushes those colors while
|
|
26
|
+
* transitions are still suppressed, then we restore them. Tying removal to
|
|
27
|
+
* the React commit (not a timer) makes it deterministic regardless of how
|
|
28
|
+
* late the re-render lands.
|
|
29
|
+
*
|
|
30
|
+
* A short fallback timer removes the class even if no re-render follows, so a
|
|
31
|
+
* theme-only-affects-CSS flip can never leave transitions permanently off.
|
|
32
|
+
*
|
|
33
|
+
* Mount once at the builder root.
|
|
34
|
+
*/
|
|
35
|
+
export function useSuppressThemeTransition(): void {
|
|
36
|
+
// Re-renders on every color-mode flip; drives the layout effect below.
|
|
37
|
+
const theme = useResolvedTheme();
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (typeof document === "undefined") return;
|
|
41
|
+
const root = document.documentElement;
|
|
42
|
+
|
|
43
|
+
let wasLight = root.classList.contains("light");
|
|
44
|
+
let fallback = 0;
|
|
45
|
+
|
|
46
|
+
const observer = new MutationObserver(() => {
|
|
47
|
+
const isLight = root.classList.contains("light");
|
|
48
|
+
if (isLight === wasLight) return; // class changed for some other reason
|
|
49
|
+
wasLight = isLight;
|
|
50
|
+
|
|
51
|
+
root.classList.add("theme-changing");
|
|
52
|
+
// Force a synchronous reflow so cascade-driven colors commit while
|
|
53
|
+
// transitions are disabled, before the browser's next paint.
|
|
54
|
+
void root.offsetHeight;
|
|
55
|
+
|
|
56
|
+
// Safety net: if no React re-render follows (theme flip touched only CSS),
|
|
57
|
+
// the layout effect won't run — drop the class anyway after a beat.
|
|
58
|
+
clearTimeout(fallback);
|
|
59
|
+
fallback = window.setTimeout(() => root.classList.remove("theme-changing"), 120);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
63
|
+
return () => {
|
|
64
|
+
observer.disconnect();
|
|
65
|
+
clearTimeout(fallback);
|
|
66
|
+
root.classList.remove("theme-changing");
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
if (typeof document === "undefined") return;
|
|
72
|
+
const root = document.documentElement;
|
|
73
|
+
if (!root.classList.contains("theme-changing")) return; // initial mount, no flip
|
|
74
|
+
// ReactFlow has committed its colorMode change by now (child layout effects
|
|
75
|
+
// run first). Flush those colors under suppression, then restore transitions.
|
|
76
|
+
void root.offsetHeight;
|
|
77
|
+
root.classList.remove("theme-changing");
|
|
78
|
+
}, [theme]);
|
|
79
|
+
}
|