@bian-womp/spark-workbench 0.1.17 → 0.1.19
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/lib/cjs/index.cjs +82 -23
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +6 -2
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts +1 -0
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +1 -1
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/misc/value.d.ts +1 -1
- package/lib/cjs/src/misc/value.d.ts.map +1 -1
- package/lib/esm/index.js +84 -25
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts +6 -2
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts +1 -0
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/hooks.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +1 -1
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/misc/value.d.ts +1 -1
- package/lib/esm/src/misc/value.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -659,6 +659,14 @@ function useWorkbenchBridge(wb) {
|
|
|
659
659
|
return;
|
|
660
660
|
if (!params.sourceHandle || !params.targetHandle)
|
|
661
661
|
return;
|
|
662
|
+
// Prevent duplicate edges between the same endpoints
|
|
663
|
+
const def = wb.export();
|
|
664
|
+
const exists = def.edges.some((e) => e.source.nodeId === params.source &&
|
|
665
|
+
e.source.handle === params.sourceHandle &&
|
|
666
|
+
e.target.nodeId === params.target &&
|
|
667
|
+
e.target.handle === params.targetHandle);
|
|
668
|
+
if (exists)
|
|
669
|
+
return;
|
|
662
670
|
wb.connect({
|
|
663
671
|
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
664
672
|
target: { nodeId: params.target, handle: params.targetHandle },
|
|
@@ -857,7 +865,7 @@ function preformatValueForDisplay(typeId, value, registry) {
|
|
|
857
865
|
}
|
|
858
866
|
return undefined;
|
|
859
867
|
}
|
|
860
|
-
function summarizeDeep(value
|
|
868
|
+
function summarizeDeep(value) {
|
|
861
869
|
// Strings: summarize data URLs and trim extremely long strings
|
|
862
870
|
if (typeof value === "string") {
|
|
863
871
|
if (value.startsWith("data:")) {
|
|
@@ -916,7 +924,9 @@ function toReactFlow(def, positions, registry, opts) {
|
|
|
916
924
|
const nodeHandleMap = {};
|
|
917
925
|
const nodes = def.nodes.map((n) => {
|
|
918
926
|
const desc = registry.nodes.get(n.typeId);
|
|
919
|
-
const inputHandles = Object.entries(desc?.inputs ?? {})
|
|
927
|
+
const inputHandles = Object.entries(desc?.inputs ?? {})
|
|
928
|
+
.filter(([id]) => !sparkGraph.isInputPrivate(desc?.inputs, id))
|
|
929
|
+
.map(([id, v]) => ({ id, typeId: sparkGraph.getInputTypeId(desc?.inputs, id) }));
|
|
920
930
|
const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId: formatDeclaredTypeSignature(typeId) }));
|
|
921
931
|
nodeHandleMap[n.nodeId] = {
|
|
922
932
|
inputs: new Set(inputHandles.map((h) => h.id)),
|
|
@@ -1110,8 +1120,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
|
|
|
1110
1120
|
layers.push(layer);
|
|
1111
1121
|
q.splice(0, q.length, ...next);
|
|
1112
1122
|
}
|
|
1113
|
-
const X =
|
|
1114
|
-
const Y =
|
|
1123
|
+
const X = 960;
|
|
1124
|
+
const Y = 480;
|
|
1115
1125
|
const pos = {};
|
|
1116
1126
|
layers.forEach((layer, layerIndex) => {
|
|
1117
1127
|
layer.forEach((id, itemIndex) => {
|
|
@@ -1521,7 +1531,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1521
1531
|
const selectedDesc = selectedNode
|
|
1522
1532
|
? registry.nodes.get(selectedNode.typeId)
|
|
1523
1533
|
: undefined;
|
|
1524
|
-
const inputHandles = Object.
|
|
1534
|
+
const inputHandles = Object.entries(selectedDesc?.inputs ?? {})
|
|
1535
|
+
.filter(([k]) => !sparkGraph.isInputPrivate(selectedDesc?.inputs, k))
|
|
1536
|
+
.map(([k]) => k);
|
|
1525
1537
|
const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
|
|
1526
1538
|
const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
|
|
1527
1539
|
const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
|
|
@@ -1568,7 +1580,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1568
1580
|
const nextDrafts = { ...drafts };
|
|
1569
1581
|
const nextOriginals = { ...originals };
|
|
1570
1582
|
for (const h of handles) {
|
|
1571
|
-
const typeId = desc?.inputs
|
|
1583
|
+
const typeId = sparkGraph.getInputTypeId(desc?.inputs, h);
|
|
1572
1584
|
const current = nodeInputs[h];
|
|
1573
1585
|
const display = safeToString(typeId, current);
|
|
1574
1586
|
const wasOriginal = originals[h];
|
|
@@ -1588,7 +1600,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1588
1600
|
const widthClass = debug ? "w-[480px]" : "w-[320px]";
|
|
1589
1601
|
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
|
|
1590
1602
|
selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
|
|
1591
|
-
const typeId = (selectedDesc?.inputs
|
|
1603
|
+
const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
|
|
1592
1604
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
|
|
1593
1605
|
e.target.handle === h);
|
|
1594
1606
|
const commonProps = {
|
|
@@ -1616,7 +1628,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
1616
1628
|
const title = inIssues
|
|
1617
1629
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1618
1630
|
.join("; ");
|
|
1619
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500
|
|
1631
|
+
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: h }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: current !== undefined && current !== null
|
|
1620
1632
|
? String(current)
|
|
1621
1633
|
: "", onChange: (e) => {
|
|
1622
1634
|
const val = e.target.value;
|
|
@@ -1661,7 +1673,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1661
1673
|
const ROW_SIZE = 22;
|
|
1662
1674
|
const maxRows = Math.max(inputEntries.length, outputEntries.length);
|
|
1663
1675
|
const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
|
|
1664
|
-
const minWidth = data.showValues ? 320 :
|
|
1676
|
+
const minWidth = data.showValues ? 320 : 240;
|
|
1665
1677
|
const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
|
|
1666
1678
|
const hasError = !!status.lastError;
|
|
1667
1679
|
const hasValidationError = validation.issues.some((i) => i.level === "error");
|
|
@@ -1694,7 +1706,13 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1694
1706
|
const title = vIssues
|
|
1695
1707
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1696
1708
|
.join("; ");
|
|
1697
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
|
|
1709
|
+
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
|
|
1710
|
+
top: topFor(i) - 8,
|
|
1711
|
+
right: "50%",
|
|
1712
|
+
whiteSpace: "nowrap",
|
|
1713
|
+
overflow: "hidden",
|
|
1714
|
+
textOverflow: "ellipsis",
|
|
1715
|
+
}, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
|
|
1698
1716
|
}), outputEntries.map((entry, i) => {
|
|
1699
1717
|
const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
|
|
1700
1718
|
const hasAny = vIssues.length > 0;
|
|
@@ -1703,7 +1721,14 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
|
|
|
1703
1721
|
.map((v) => `${v.code}: ${v.message}`)
|
|
1704
1722
|
.join("; ");
|
|
1705
1723
|
const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
|
|
1706
|
-
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
|
|
1724
|
+
return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
|
|
1725
|
+
top: topFor(i) - 8,
|
|
1726
|
+
textAlign: "right",
|
|
1727
|
+
left: "50%",
|
|
1728
|
+
whiteSpace: "nowrap",
|
|
1729
|
+
overflow: "hidden",
|
|
1730
|
+
textOverflow: "ellipsis",
|
|
1731
|
+
}, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, resolved.typeId && (jsxRuntime.jsxs("span", { className: "ml-1 opacity-60", children: ["(", resolved.typeId, ")"] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
|
|
1707
1732
|
})] }));
|
|
1708
1733
|
});
|
|
1709
1734
|
DefaultNode.displayName = "DefaultNode";
|
|
@@ -1842,11 +1867,21 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
|
|
|
1842
1867
|
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
|
|
1843
1868
|
}
|
|
1844
1869
|
|
|
1845
|
-
|
|
1870
|
+
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, ref) => {
|
|
1846
1871
|
const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
|
|
1847
1872
|
const ioValues = { inputs: inputsMap, outputs: outputsMap };
|
|
1848
1873
|
const nodeValidation = validationByNode;
|
|
1849
1874
|
const edgeValidation = validationByEdge.errors;
|
|
1875
|
+
// Expose imperative API
|
|
1876
|
+
const rfInstanceRef = React.useRef(null);
|
|
1877
|
+
React.useImperativeHandle(ref, () => ({
|
|
1878
|
+
fitView: () => {
|
|
1879
|
+
try {
|
|
1880
|
+
rfInstanceRef.current?.fitView({ padding: 0.2 });
|
|
1881
|
+
}
|
|
1882
|
+
catch { }
|
|
1883
|
+
},
|
|
1884
|
+
}));
|
|
1850
1885
|
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
|
|
1851
1886
|
const { nodeTypes, resolveNodeType } = React.useMemo(() => {
|
|
1852
1887
|
// Build nodeTypes map using UI extension registry
|
|
@@ -1922,8 +1957,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
|
|
|
1922
1957
|
const addNodeAt = (typeId, pos) => {
|
|
1923
1958
|
wb.addNode({ typeId, position: pos });
|
|
1924
1959
|
};
|
|
1925
|
-
return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
|
|
1926
|
-
}
|
|
1960
|
+
return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, onInit: (inst) => (rfInstanceRef.current = inst), children: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
|
|
1961
|
+
});
|
|
1927
1962
|
|
|
1928
1963
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
|
|
1929
1964
|
const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
|
|
@@ -1931,7 +1966,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1931
1966
|
const selectedDesc = selectedNode
|
|
1932
1967
|
? registry.nodes.get(selectedNode.typeId)
|
|
1933
1968
|
: undefined;
|
|
1934
|
-
const [exampleState, setExampleState] = React.useState(example ?? "
|
|
1969
|
+
const [exampleState, setExampleState] = React.useState(example ?? "");
|
|
1935
1970
|
const defaultExamples = React.useMemo(() => [
|
|
1936
1971
|
{
|
|
1937
1972
|
id: "simple",
|
|
@@ -1973,6 +2008,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1973
2008
|
}, [overrides, defaultExamples]);
|
|
1974
2009
|
const lastAutoLaunched = React.useRef(undefined);
|
|
1975
2010
|
const autoLayoutRan = React.useRef(false);
|
|
2011
|
+
const canvasRef = React.useRef(null);
|
|
1976
2012
|
const applyExample = React.useCallback(async (key) => {
|
|
1977
2013
|
if (runner.isRunning()) {
|
|
1978
2014
|
alert(`Stop engine before switching example.`);
|
|
@@ -1993,6 +2029,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
1993
2029
|
setExampleState(key);
|
|
1994
2030
|
onExampleChange?.(key);
|
|
1995
2031
|
}, [runner, wb, onExampleChange, runAutoLayout, examples, setRegistry]);
|
|
2032
|
+
const downloadGraph = React.useCallback(() => {
|
|
2033
|
+
try {
|
|
2034
|
+
const def = wb.export();
|
|
2035
|
+
const pretty = JSON.stringify(def, null, 2);
|
|
2036
|
+
const blob = new Blob([pretty], { type: "application/json" });
|
|
2037
|
+
const url = URL.createObjectURL(blob);
|
|
2038
|
+
const a = document.createElement("a");
|
|
2039
|
+
const d = new Date();
|
|
2040
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2041
|
+
const ts = `${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
|
|
2042
|
+
a.href = url;
|
|
2043
|
+
a.download = `spark-graph-${ts}.json`;
|
|
2044
|
+
document.body.appendChild(a);
|
|
2045
|
+
a.click();
|
|
2046
|
+
a.remove();
|
|
2047
|
+
URL.revokeObjectURL(url);
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
alert(String(err?.message ?? err));
|
|
2051
|
+
}
|
|
2052
|
+
}, [wb]);
|
|
1996
2053
|
const hydrateFromBackend = React.useCallback(async (kind, base) => {
|
|
1997
2054
|
try {
|
|
1998
2055
|
const transport = kind === "remote-http"
|
|
@@ -2066,7 +2123,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2066
2123
|
}, [setRegistry, wb]);
|
|
2067
2124
|
// Ensure initial example is loaded (and sync when example prop changes)
|
|
2068
2125
|
React.useEffect(() => {
|
|
2069
|
-
|
|
2126
|
+
if (!example)
|
|
2127
|
+
return;
|
|
2128
|
+
applyExample(example);
|
|
2070
2129
|
}, [example, wb]);
|
|
2071
2130
|
React.useEffect(() => {
|
|
2072
2131
|
if (!engine)
|
|
@@ -2137,7 +2196,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2137
2196
|
break;
|
|
2138
2197
|
}
|
|
2139
2198
|
case "base.bool": {
|
|
2140
|
-
value =
|
|
2199
|
+
value = raw === "true" || raw === "1" ? true : false;
|
|
2141
2200
|
break;
|
|
2142
2201
|
}
|
|
2143
2202
|
case "base.string": {
|
|
@@ -2274,14 +2333,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2274
2333
|
return overrides.toElement(baseToElement, { registry });
|
|
2275
2334
|
return baseToElement;
|
|
2276
2335
|
}, [overrides, baseToElement, registry]);
|
|
2277
|
-
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.
|
|
2336
|
+
return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2278
2337
|
? "Stop engine before switching example"
|
|
2279
|
-
: undefined, children: examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id))) }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2338
|
+
: undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
|
|
2280
2339
|
? "Stop engine before switching backend"
|
|
2281
2340
|
: undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
|
|
2282
2341
|
const kind = e.target.value || undefined;
|
|
2283
2342
|
onEngineChange?.(kind);
|
|
2284
|
-
}, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { onClick: () => {
|
|
2343
|
+
}, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => {
|
|
2285
2344
|
const kind = engine;
|
|
2286
2345
|
if (!kind)
|
|
2287
2346
|
return alert("Select an engine first.");
|
|
@@ -2291,7 +2350,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
2291
2350
|
catch (err) {
|
|
2292
2351
|
alert(String(err?.message ?? err));
|
|
2293
2352
|
}
|
|
2294
|
-
}, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
|
|
2353
|
+
}, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
|
|
2295
2354
|
}
|
|
2296
2355
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
|
|
2297
2356
|
const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
|
|
@@ -2307,9 +2366,9 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
2307
2366
|
// Allow external UI registration (e.g., node renderers) with access to wb
|
|
2308
2367
|
React.useEffect(() => {
|
|
2309
2368
|
const baseRegisterUI = (_wb) => { };
|
|
2310
|
-
overrides?.registerUI?.(baseRegisterUI, { wb });
|
|
2369
|
+
overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
|
|
2311
2370
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2312
|
-
}, [wb, overrides]);
|
|
2371
|
+
}, [wb, runner, overrides]);
|
|
2313
2372
|
return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
|
|
2314
2373
|
if (runner.isRunning())
|
|
2315
2374
|
runner.dispose();
|