@bian-womp/spark-workbench 0.2.36 → 0.2.38
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 +405 -221
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/index.d.ts +1 -0
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/cjs/src/misc/Inspector.d.ts +1 -2
- package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +4 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/mapping.d.ts +19 -0
- package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +402 -223
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/index.d.ts +1 -0
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
- package/lib/esm/src/misc/Inspector.d.ts +1 -2
- package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +4 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/mapping.d.ts +19 -0
- package/lib/esm/src/misc/mapping.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +32 -17
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/esm/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { GraphBuilder, StepEngine, HybridEngine, PullEngine, BatchedEngine, PushEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
|
|
2
|
-
import {
|
|
2
|
+
import { RuntimeApiClient } from '@bian-womp/spark-remote';
|
|
3
3
|
import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
|
|
4
4
|
import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
|
|
5
|
+
import cx from 'classnames';
|
|
5
6
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
7
|
import { XCircleIcon, WarningCircleIcon, CopyIcon, TrashIcon, XIcon, ArrowClockwiseIcon, PlugsConnectedIcon, ClockClockwiseIcon, WifiHighIcon, WifiSlashIcon, PlayPauseIcon, LightningIcon, StopIcon, PlayIcon, TreeStructureIcon, CornersOutIcon, DownloadSimpleIcon, DownloadIcon, UploadIcon, BugBeetleIcon, ListBulletsIcon } from '@phosphor-icons/react';
|
|
7
|
-
import cx from 'classnames';
|
|
8
8
|
import isEqual from 'lodash/isEqual';
|
|
9
9
|
|
|
10
10
|
class DefaultUIExtensionRegistry {
|
|
@@ -341,7 +341,12 @@ class AbstractGraphRunner {
|
|
|
341
341
|
setInput(nodeId, handle, value) {
|
|
342
342
|
if (!this.stagedInputs[nodeId])
|
|
343
343
|
this.stagedInputs[nodeId] = {};
|
|
344
|
-
|
|
344
|
+
if (value === undefined) {
|
|
345
|
+
delete this.stagedInputs[nodeId][handle];
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
349
|
+
}
|
|
345
350
|
if (this.engine) {
|
|
346
351
|
this.engine.setInput(nodeId, handle, value);
|
|
347
352
|
}
|
|
@@ -359,7 +364,14 @@ class AbstractGraphRunner {
|
|
|
359
364
|
return;
|
|
360
365
|
if (!this.stagedInputs[nodeId])
|
|
361
366
|
this.stagedInputs[nodeId] = {};
|
|
362
|
-
|
|
367
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
368
|
+
if (value === undefined) {
|
|
369
|
+
delete this.stagedInputs[nodeId][handle];
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this.stagedInputs[nodeId][handle] = value;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
363
375
|
if (this.engine) {
|
|
364
376
|
// Running: set all inputs
|
|
365
377
|
this.engine.setInputs(nodeId, inputs);
|
|
@@ -588,8 +600,200 @@ class LocalGraphRunner extends AbstractGraphRunner {
|
|
|
588
600
|
}
|
|
589
601
|
|
|
590
602
|
class RemoteGraphRunner extends AbstractGraphRunner {
|
|
603
|
+
/**
|
|
604
|
+
* Fetch full registry description from remote and register it locally.
|
|
605
|
+
* Called automatically on first connection with retry mechanism.
|
|
606
|
+
*/
|
|
607
|
+
async fetchRegistry(client, attempt = 1) {
|
|
608
|
+
if (this.registryFetching) {
|
|
609
|
+
// Already fetching, don't start another fetch
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.registryFetching = true;
|
|
613
|
+
try {
|
|
614
|
+
const desc = await client.describeRegistry();
|
|
615
|
+
// Register types
|
|
616
|
+
for (const t of desc.types) {
|
|
617
|
+
if (t.options) {
|
|
618
|
+
this.registry.registerEnum({
|
|
619
|
+
id: t.id,
|
|
620
|
+
options: t.options,
|
|
621
|
+
bakeTarget: t.bakeTarget,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
if (!this.registry.types.has(t.id)) {
|
|
626
|
+
this.registry.registerType({
|
|
627
|
+
id: t.id,
|
|
628
|
+
displayName: t.displayName,
|
|
629
|
+
validate: (_v) => true,
|
|
630
|
+
bakeTarget: t.bakeTarget,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Register categories
|
|
636
|
+
for (const c of desc.categories || []) {
|
|
637
|
+
if (!this.registry.categories.has(c.id)) {
|
|
638
|
+
// Create placeholder category descriptor
|
|
639
|
+
const category = {
|
|
640
|
+
id: c.id,
|
|
641
|
+
displayName: c.displayName,
|
|
642
|
+
createRuntime: () => ({
|
|
643
|
+
async onInputsChanged() { },
|
|
644
|
+
}),
|
|
645
|
+
policy: { asyncConcurrency: "switch" },
|
|
646
|
+
};
|
|
647
|
+
this.registry.categories.register(category);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Register coercions
|
|
651
|
+
for (const c of desc.coercions) {
|
|
652
|
+
if (c.async) {
|
|
653
|
+
this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
654
|
+
nonTransitive: c.nonTransitive,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
this.registry.registerCoercion(c.from, c.to, (v) => v, {
|
|
659
|
+
nonTransitive: c.nonTransitive,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Register nodes
|
|
664
|
+
for (const n of desc.nodes) {
|
|
665
|
+
if (!this.registry.nodes.has(n.id)) {
|
|
666
|
+
this.registry.registerNode({
|
|
667
|
+
id: n.id,
|
|
668
|
+
categoryId: n.categoryId,
|
|
669
|
+
displayName: n.displayName,
|
|
670
|
+
inputs: n.inputs || {},
|
|
671
|
+
outputs: n.outputs || {},
|
|
672
|
+
impl: () => { },
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
this.registryFetched = true;
|
|
677
|
+
this.registryFetching = false;
|
|
678
|
+
this.emit("registry", this.registry);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
this.registryFetching = false;
|
|
682
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
683
|
+
// Retry with exponential backoff if attempts remaining
|
|
684
|
+
if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
|
|
685
|
+
const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
686
|
+
console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
|
|
687
|
+
// Emit error event for UI feedback
|
|
688
|
+
this.emit("error", {
|
|
689
|
+
kind: "registry",
|
|
690
|
+
message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
|
|
691
|
+
err: error,
|
|
692
|
+
attempt,
|
|
693
|
+
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
694
|
+
});
|
|
695
|
+
// Retry after delay
|
|
696
|
+
setTimeout(() => {
|
|
697
|
+
this.fetchRegistry(client, attempt + 1).catch(() => {
|
|
698
|
+
// Final failure handled below
|
|
699
|
+
});
|
|
700
|
+
}, delayMs);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
// Max attempts reached, emit final error
|
|
704
|
+
console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
|
|
705
|
+
this.emit("error", {
|
|
706
|
+
kind: "registry",
|
|
707
|
+
message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
|
|
708
|
+
err: error,
|
|
709
|
+
attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
710
|
+
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Build RuntimeApiClient config from RemoteExecutionBackend config.
|
|
717
|
+
*/
|
|
718
|
+
buildClientConfig(backend) {
|
|
719
|
+
if (backend.kind === "remote-http") {
|
|
720
|
+
return {
|
|
721
|
+
kind: "remote-http",
|
|
722
|
+
baseUrl: backend.baseUrl,
|
|
723
|
+
connectOptions: backend.connectOptions,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
return {
|
|
728
|
+
kind: "remote-ws",
|
|
729
|
+
url: backend.url,
|
|
730
|
+
connectOptions: backend.connectOptions,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Setup event subscriptions for the client.
|
|
736
|
+
*/
|
|
737
|
+
setupClientSubscriptions(client, backend) {
|
|
738
|
+
// Subscribe to custom events (for additional listeners if needed)
|
|
739
|
+
if (backend.onCustomEvent) {
|
|
740
|
+
this.customEventUnsubscribe = client.subscribeCustomEvents(backend.onCustomEvent);
|
|
741
|
+
}
|
|
742
|
+
// Subscribe to transport status changes
|
|
743
|
+
// Convert RuntimeApiClient.TransportStatus to IGraphRunner.TransportStatus
|
|
744
|
+
this.transportStatusUnsubscribe = client.onTransportStatus((status) => {
|
|
745
|
+
// Map remote-unix to undefined since RemoteGraphRunner doesn't support it
|
|
746
|
+
const mappedKind = status.kind === "remote-unix" ? undefined : status.kind;
|
|
747
|
+
this.emit("transport", {
|
|
748
|
+
state: status.state,
|
|
749
|
+
kind: mappedKind,
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
// Ensure remote client
|
|
754
|
+
async ensureClient() {
|
|
755
|
+
if (this.disposed) {
|
|
756
|
+
throw new Error("Cannot ensure client: RemoteGraphRunner has been disposed");
|
|
757
|
+
}
|
|
758
|
+
if (this.client)
|
|
759
|
+
return this.client;
|
|
760
|
+
// If already connecting, wait for that connection to complete
|
|
761
|
+
if (this.clientPromise)
|
|
762
|
+
return this.clientPromise;
|
|
763
|
+
const backend = this.backend;
|
|
764
|
+
// Create connection promise to prevent concurrent connections
|
|
765
|
+
this.clientPromise = (async () => {
|
|
766
|
+
// Build client config from backend config
|
|
767
|
+
const clientConfig = this.buildClientConfig(backend);
|
|
768
|
+
// Create client with custom event handler if provided
|
|
769
|
+
const client = new RuntimeApiClient(clientConfig, {
|
|
770
|
+
onCustomEvent: backend.onCustomEvent,
|
|
771
|
+
});
|
|
772
|
+
// Setup event subscriptions
|
|
773
|
+
this.setupClientSubscriptions(client, backend);
|
|
774
|
+
// Connect the client (this will create and connect transport)
|
|
775
|
+
await client.connect();
|
|
776
|
+
this.client = client;
|
|
777
|
+
this.valueCache.clear();
|
|
778
|
+
this.listenersBound = false;
|
|
779
|
+
// Auto-fetch registry on first connection (only once)
|
|
780
|
+
if (!this.registryFetched && !this.registryFetching) {
|
|
781
|
+
// Log loading state (UI can listen to transport status for loading indication)
|
|
782
|
+
console.info("Loading registry from remote...");
|
|
783
|
+
this.fetchRegistry(client).catch((err) => {
|
|
784
|
+
console.error("[RemoteGraphRunner] Failed to fetch registry:", err);
|
|
785
|
+
// Error handling is done inside fetchRegistry, but we catch unhandled rejections
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
// Clear promise on success
|
|
789
|
+
this.clientPromise = undefined;
|
|
790
|
+
return client;
|
|
791
|
+
})();
|
|
792
|
+
return this.clientPromise;
|
|
793
|
+
}
|
|
591
794
|
constructor(registry, backend) {
|
|
592
795
|
super(registry, backend);
|
|
796
|
+
this.disposed = false;
|
|
593
797
|
this.valueCache = new Map();
|
|
594
798
|
this.listenersBound = false;
|
|
595
799
|
this.registryFetched = false;
|
|
@@ -598,8 +802,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
598
802
|
this.INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
599
803
|
// Auto-handle registry-changed invalidations from remote
|
|
600
804
|
// We listen on invalidate and if reason matches, we rehydrate registry and emit a registry event
|
|
601
|
-
this.
|
|
602
|
-
const eng =
|
|
805
|
+
this.ensureClient().then(async (client) => {
|
|
806
|
+
const eng = client.getEngine();
|
|
603
807
|
if (!this.listenersBound) {
|
|
604
808
|
eng.on("invalidate", async (e) => {
|
|
605
809
|
if (e.reason === "registry-changed") {
|
|
@@ -663,9 +867,9 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
663
867
|
}
|
|
664
868
|
update(def) {
|
|
665
869
|
// Remote: forward update; ignore errors (fire-and-forget)
|
|
666
|
-
this.
|
|
870
|
+
this.ensureClient().then(async (client) => {
|
|
667
871
|
try {
|
|
668
|
-
await
|
|
872
|
+
await client.update(def);
|
|
669
873
|
this.emit("invalidate", { reason: "graph-updated" });
|
|
670
874
|
this.lastDef = def;
|
|
671
875
|
}
|
|
@@ -675,14 +879,14 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
675
879
|
launch(def, opts) {
|
|
676
880
|
super.launch(def, opts);
|
|
677
881
|
// Remote: build remotely then launch
|
|
678
|
-
this.
|
|
679
|
-
await
|
|
882
|
+
this.ensureClient().then(async (client) => {
|
|
883
|
+
await client.build(def);
|
|
680
884
|
// Signal UI after remote build as well
|
|
681
885
|
this.emit("invalidate", { reason: "graph-built" });
|
|
682
886
|
this.lastDef = def;
|
|
683
887
|
// Hydrate current remote inputs/outputs (including defaults) into cache
|
|
684
888
|
try {
|
|
685
|
-
const snap = await
|
|
889
|
+
const snap = await client.snapshot();
|
|
686
890
|
for (const [nodeId, map] of Object.entries(snap.inputs || {})) {
|
|
687
891
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
688
892
|
this.valueCache.set(`${nodeId}.${handle}`, {
|
|
@@ -705,7 +909,47 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
705
909
|
catch {
|
|
706
910
|
console.error("Failed to hydrate remote inputs/outputs");
|
|
707
911
|
}
|
|
708
|
-
const eng =
|
|
912
|
+
const eng = client.getEngine();
|
|
913
|
+
if (!this.listenersBound) {
|
|
914
|
+
eng.on("value", (e) => {
|
|
915
|
+
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
916
|
+
io: e.io,
|
|
917
|
+
value: e.value,
|
|
918
|
+
runtimeTypeId: e.runtimeTypeId,
|
|
919
|
+
});
|
|
920
|
+
this.emit("value", e);
|
|
921
|
+
});
|
|
922
|
+
eng.on("error", (e) => this.emit("error", e));
|
|
923
|
+
eng.on("invalidate", (e) => this.emit("invalidate", e));
|
|
924
|
+
eng.on("stats", (e) => this.emit("stats", e));
|
|
925
|
+
this.listenersBound = true;
|
|
926
|
+
}
|
|
927
|
+
this.engine = eng;
|
|
928
|
+
this.engine.launch(opts.invalidate);
|
|
929
|
+
this.runningKind = "push";
|
|
930
|
+
this.emit("status", { running: true, engine: this.runningKind });
|
|
931
|
+
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
932
|
+
this.engine.setInputs(nodeId, map);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Launch using an existing backend runtime that has already been built and hydrated.
|
|
938
|
+
* This is used when resuming from a snapshot where the backend has already applied
|
|
939
|
+
* ApplySnapshotFull (which builds the graph and hydrates inputs/outputs).
|
|
940
|
+
* Unlike launch(), this method does NOT call client.build() to avoid destroying
|
|
941
|
+
* the runtime state that was just restored.
|
|
942
|
+
*/
|
|
943
|
+
launchExisting(def, opts) {
|
|
944
|
+
super.launch(def, opts);
|
|
945
|
+
// Remote: attach to existing runtime and launch (do NOT rebuild)
|
|
946
|
+
this.ensureClient().then(async (client) => {
|
|
947
|
+
// NOTE: We do NOT call client.build(def) here because the backend runtime
|
|
948
|
+
// has already been built and hydrated via ApplySnapshotFull.
|
|
949
|
+
// Calling build() would create a new runtime and lose the restored state.
|
|
950
|
+
this.lastDef = def;
|
|
951
|
+
// Attach to the existing engine
|
|
952
|
+
const eng = client.getEngine();
|
|
709
953
|
if (!this.listenersBound) {
|
|
710
954
|
eng.on("value", (e) => {
|
|
711
955
|
this.valueCache.set(`${e.nodeId}.${e.handle}`, {
|
|
@@ -739,26 +983,26 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
739
983
|
console.warn("Unsupported operation for remote runner");
|
|
740
984
|
}
|
|
741
985
|
triggerExternal(nodeId, event) {
|
|
742
|
-
this.
|
|
986
|
+
this.ensureClient().then(async (client) => {
|
|
743
987
|
try {
|
|
744
|
-
await
|
|
988
|
+
await client.getEngine().triggerExternal(nodeId, event);
|
|
745
989
|
}
|
|
746
990
|
catch { }
|
|
747
991
|
});
|
|
748
992
|
}
|
|
749
993
|
async coerce(from, to, value) {
|
|
750
|
-
const
|
|
994
|
+
const client = await this.ensureClient();
|
|
751
995
|
try {
|
|
752
|
-
return await
|
|
996
|
+
return await client.coerce(from, to, value);
|
|
753
997
|
}
|
|
754
998
|
catch {
|
|
755
999
|
return value;
|
|
756
1000
|
}
|
|
757
1001
|
}
|
|
758
1002
|
async snapshotFull() {
|
|
759
|
-
const
|
|
1003
|
+
const client = await this.ensureClient();
|
|
760
1004
|
try {
|
|
761
|
-
return await
|
|
1005
|
+
return await client.snapshotFull();
|
|
762
1006
|
}
|
|
763
1007
|
catch {
|
|
764
1008
|
return { def: undefined, environment: {}, inputs: {}, outputs: {} };
|
|
@@ -766,17 +1010,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
766
1010
|
}
|
|
767
1011
|
async applySnapshotFull(payload) {
|
|
768
1012
|
// Hydrate local cache first so UI can display values immediately
|
|
769
|
-
this.
|
|
1013
|
+
this.hydrateSnapshotFull(payload);
|
|
770
1014
|
// Then sync with backend
|
|
771
|
-
const
|
|
772
|
-
await
|
|
1015
|
+
const client = await this.ensureClient();
|
|
1016
|
+
await client.applySnapshotFull(payload);
|
|
773
1017
|
}
|
|
774
1018
|
/**
|
|
775
1019
|
* Hydrates the local valueCache from a snapshot and emits value events.
|
|
776
1020
|
* This ensures the UI can display inputs/outputs immediately without waiting
|
|
777
1021
|
* for value events from the remote backend.
|
|
778
1022
|
*/
|
|
779
|
-
|
|
1023
|
+
hydrateSnapshotFull(snapshot) {
|
|
780
1024
|
// Hydrate inputs
|
|
781
1025
|
for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
|
|
782
1026
|
for (const [handle, value] of Object.entries(map || {})) {
|
|
@@ -799,20 +1043,25 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
799
1043
|
}
|
|
800
1044
|
}
|
|
801
1045
|
setEnvironment(env, opts) {
|
|
802
|
-
|
|
803
|
-
if (
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1046
|
+
// Use client if available, otherwise ensure client and then set environment
|
|
1047
|
+
if (this.client) {
|
|
1048
|
+
this.client.setEnvironment(env, opts).catch(() => { });
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
// If client not ready yet, ensure it and then set environment
|
|
1052
|
+
this.ensureClient()
|
|
1053
|
+
.then((client) => {
|
|
1054
|
+
client.setEnvironment(env, opts).catch(() => { });
|
|
1055
|
+
})
|
|
1056
|
+
.catch(() => { });
|
|
1057
|
+
}
|
|
811
1058
|
}
|
|
812
1059
|
getEnvironment() {
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
1060
|
+
// Interface requires sync return, but RuntimeApiClient.getEnvironment() is async.
|
|
1061
|
+
// Returns undefined synchronously; callers needing the actual value should:
|
|
1062
|
+
// - Use snapshotFull() which includes environment
|
|
1063
|
+
// - Call client.getEnvironment() directly if they have access to the client
|
|
1064
|
+
// This is a limitation of the sync interface signature.
|
|
816
1065
|
return undefined;
|
|
817
1066
|
}
|
|
818
1067
|
getOutputs(def) {
|
|
@@ -876,183 +1125,37 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
876
1125
|
return out;
|
|
877
1126
|
}
|
|
878
1127
|
dispose() {
|
|
1128
|
+
// Idempotent: allow multiple calls safely
|
|
1129
|
+
if (this.disposed)
|
|
1130
|
+
return;
|
|
1131
|
+
this.disposed = true;
|
|
879
1132
|
super.dispose();
|
|
880
|
-
|
|
881
|
-
this.
|
|
1133
|
+
// Clear client promise if any
|
|
1134
|
+
this.clientPromise = undefined;
|
|
1135
|
+
// Unsubscribe from custom events and transport status
|
|
1136
|
+
if (this.customEventUnsubscribe) {
|
|
1137
|
+
this.customEventUnsubscribe();
|
|
1138
|
+
this.customEventUnsubscribe = undefined;
|
|
1139
|
+
}
|
|
1140
|
+
if (this.transportStatusUnsubscribe) {
|
|
1141
|
+
this.transportStatusUnsubscribe();
|
|
1142
|
+
this.transportStatusUnsubscribe = undefined;
|
|
1143
|
+
}
|
|
1144
|
+
// Dispose client (which will close transport)
|
|
1145
|
+
const clientToDispose = this.client;
|
|
1146
|
+
this.client = undefined;
|
|
882
1147
|
this.registryFetched = false; // Reset so registry is fetched again on reconnect
|
|
883
1148
|
this.registryFetching = false; // Reset fetching state
|
|
1149
|
+
if (clientToDispose) {
|
|
1150
|
+
clientToDispose.dispose().catch((err) => {
|
|
1151
|
+
console.warn("[RemoteGraphRunner] Error disposing client:", err);
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
884
1154
|
this.emit("transport", {
|
|
885
1155
|
state: "disconnected",
|
|
886
1156
|
kind: this.backend.kind,
|
|
887
1157
|
});
|
|
888
1158
|
}
|
|
889
|
-
/**
|
|
890
|
-
* Fetch full registry description from remote and register it locally.
|
|
891
|
-
* Called automatically on first connection with retry mechanism.
|
|
892
|
-
*/
|
|
893
|
-
async fetchRegistry(runner, attempt = 1) {
|
|
894
|
-
if (this.registryFetching) {
|
|
895
|
-
// Already fetching, don't start another fetch
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
this.registryFetching = true;
|
|
899
|
-
try {
|
|
900
|
-
const desc = await runner.describeRegistry();
|
|
901
|
-
// Register types
|
|
902
|
-
for (const t of desc.types) {
|
|
903
|
-
if (t.options) {
|
|
904
|
-
this.registry.registerEnum({
|
|
905
|
-
id: t.id,
|
|
906
|
-
options: t.options,
|
|
907
|
-
bakeTarget: t.bakeTarget,
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
else {
|
|
911
|
-
if (!this.registry.types.has(t.id)) {
|
|
912
|
-
this.registry.registerType({
|
|
913
|
-
id: t.id,
|
|
914
|
-
displayName: t.displayName,
|
|
915
|
-
validate: (_v) => true,
|
|
916
|
-
bakeTarget: t.bakeTarget,
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
// Register categories
|
|
922
|
-
for (const c of desc.categories || []) {
|
|
923
|
-
if (!this.registry.categories.has(c.id)) {
|
|
924
|
-
// Create placeholder category descriptor
|
|
925
|
-
const category = {
|
|
926
|
-
id: c.id,
|
|
927
|
-
displayName: c.displayName,
|
|
928
|
-
createRuntime: () => ({
|
|
929
|
-
async onInputsChanged() { },
|
|
930
|
-
}),
|
|
931
|
-
policy: { asyncConcurrency: "switch" },
|
|
932
|
-
};
|
|
933
|
-
this.registry.categories.register(category);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
// Register coercions
|
|
937
|
-
for (const c of desc.coercions) {
|
|
938
|
-
if (c.async) {
|
|
939
|
-
this.registry.registerAsyncCoercion(c.from, c.to, async (v) => v, {
|
|
940
|
-
nonTransitive: c.nonTransitive,
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
this.registry.registerCoercion(c.from, c.to, (v) => v, {
|
|
945
|
-
nonTransitive: c.nonTransitive,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
// Register nodes
|
|
950
|
-
for (const n of desc.nodes) {
|
|
951
|
-
if (!this.registry.nodes.has(n.id)) {
|
|
952
|
-
this.registry.registerNode({
|
|
953
|
-
id: n.id,
|
|
954
|
-
categoryId: n.categoryId,
|
|
955
|
-
displayName: n.displayName,
|
|
956
|
-
inputs: n.inputs || {},
|
|
957
|
-
outputs: n.outputs || {},
|
|
958
|
-
impl: () => { },
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
this.registryFetched = true;
|
|
963
|
-
this.registryFetching = false;
|
|
964
|
-
this.emit("registry", this.registry);
|
|
965
|
-
}
|
|
966
|
-
catch (err) {
|
|
967
|
-
this.registryFetching = false;
|
|
968
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
969
|
-
// Retry with exponential backoff if attempts remaining
|
|
970
|
-
if (attempt < this.MAX_REGISTRY_FETCH_ATTEMPTS) {
|
|
971
|
-
const delayMs = this.INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
972
|
-
console.warn(`Failed to fetch registry (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying in ${delayMs}ms...`, error);
|
|
973
|
-
// Emit error event for UI feedback
|
|
974
|
-
this.emit("error", {
|
|
975
|
-
kind: "registry",
|
|
976
|
-
message: `Registry fetch failed (attempt ${attempt}/${this.MAX_REGISTRY_FETCH_ATTEMPTS}), retrying...`,
|
|
977
|
-
err: error,
|
|
978
|
-
attempt,
|
|
979
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
980
|
-
});
|
|
981
|
-
// Retry after delay
|
|
982
|
-
setTimeout(() => {
|
|
983
|
-
this.fetchRegistry(runner, attempt + 1).catch(() => {
|
|
984
|
-
// Final failure handled below
|
|
985
|
-
});
|
|
986
|
-
}, delayMs);
|
|
987
|
-
}
|
|
988
|
-
else {
|
|
989
|
-
// Max attempts reached, emit final error
|
|
990
|
-
console.error(`Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts:`, error);
|
|
991
|
-
this.emit("error", {
|
|
992
|
-
kind: "registry",
|
|
993
|
-
message: `Failed to fetch registry after ${this.MAX_REGISTRY_FETCH_ATTEMPTS} attempts. Please check your connection and try refreshing.`,
|
|
994
|
-
err: error,
|
|
995
|
-
attempt: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
996
|
-
maxAttempts: this.MAX_REGISTRY_FETCH_ATTEMPTS,
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
// Ensure remote transport/runner
|
|
1002
|
-
async ensureRemoteRunner() {
|
|
1003
|
-
if (this.runner)
|
|
1004
|
-
return this.runner;
|
|
1005
|
-
let transport;
|
|
1006
|
-
const kind = this.backend.kind;
|
|
1007
|
-
const backend = this.backend;
|
|
1008
|
-
const connectOptions = backend.connectOptions;
|
|
1009
|
-
this.emit("transport", { state: "connecting", kind });
|
|
1010
|
-
if (backend.kind === "remote-http") {
|
|
1011
|
-
if (!HttpPollingTransport)
|
|
1012
|
-
throw new Error("HttpPollingTransport not available");
|
|
1013
|
-
transport = new HttpPollingTransport(backend.baseUrl);
|
|
1014
|
-
await transport.connect(connectOptions);
|
|
1015
|
-
}
|
|
1016
|
-
else if (backend.kind === "remote-ws") {
|
|
1017
|
-
if (!WebSocketTransport)
|
|
1018
|
-
throw new Error("WebSocketTransport not available");
|
|
1019
|
-
transport = new WebSocketTransport(backend.url);
|
|
1020
|
-
await transport.connect(connectOptions);
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
throw new Error("Remote backend not configured");
|
|
1024
|
-
}
|
|
1025
|
-
// Subscribe to custom events if handler provided
|
|
1026
|
-
if (backend.onCustomEvent) {
|
|
1027
|
-
transport.subscribe((event) => {
|
|
1028
|
-
// Filter out standard runtime events, pass others to custom handler
|
|
1029
|
-
const msg = event.message;
|
|
1030
|
-
if (msg && typeof msg === "object" && "type" in msg) {
|
|
1031
|
-
const type = msg.type;
|
|
1032
|
-
// Standard runtime events: stats, value, error, invalidate
|
|
1033
|
-
// Custom events are anything else (e.g., flow-opened, flow-latest)
|
|
1034
|
-
if (!["stats", "value", "error", "invalidate"].includes(type)) {
|
|
1035
|
-
backend.onCustomEvent?.(event);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
const runner = new RemoteRunner(transport);
|
|
1041
|
-
this.runner = runner;
|
|
1042
|
-
this.transport = transport;
|
|
1043
|
-
this.valueCache.clear();
|
|
1044
|
-
this.listenersBound = false;
|
|
1045
|
-
this.emit("transport", { state: "connected", kind });
|
|
1046
|
-
// Auto-fetch registry on first connection (only once)
|
|
1047
|
-
if (!this.registryFetched && !this.registryFetching) {
|
|
1048
|
-
// Log loading state (UI can listen to transport status for loading indication)
|
|
1049
|
-
console.info("Loading registry from remote...");
|
|
1050
|
-
this.fetchRegistry(runner).catch(() => {
|
|
1051
|
-
// Error handling is done inside fetchRegistry
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
return runner;
|
|
1055
|
-
}
|
|
1056
1159
|
}
|
|
1057
1160
|
|
|
1058
1161
|
function useWorkbenchBridge(wb) {
|
|
@@ -1720,6 +1823,33 @@ function getNodeBorderClassNames(args) {
|
|
|
1720
1823
|
: "";
|
|
1721
1824
|
return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
|
|
1722
1825
|
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Shared utility to generate handle className based on validation and value state.
|
|
1828
|
+
* - Linked handles (with inbound edges) get black borders
|
|
1829
|
+
* - Handles with values (but not linked) get darker gray borders
|
|
1830
|
+
* - Handles with only defaults (no value, not linked) get lighter gray borders
|
|
1831
|
+
* - Validation errors (red/amber) take precedence over value-based styling.
|
|
1832
|
+
*/
|
|
1833
|
+
function getHandleClassName(args) {
|
|
1834
|
+
const { kind, id, validation } = args;
|
|
1835
|
+
const vIssues = (kind === "input" ? validation.inputs : validation.outputs)?.filter((v) => v.handle === id) || [];
|
|
1836
|
+
const hasAny = vIssues.length > 0;
|
|
1837
|
+
const hasErr = vIssues.some((v) => v.level === "error");
|
|
1838
|
+
// Determine border color based on priority:
|
|
1839
|
+
// 1. Validation errors (red/amber) - highest priority
|
|
1840
|
+
// 2. Gray border
|
|
1841
|
+
let borderColor;
|
|
1842
|
+
if (hasAny && hasErr) {
|
|
1843
|
+
borderColor = "!border-red-500";
|
|
1844
|
+
}
|
|
1845
|
+
else if (hasAny) {
|
|
1846
|
+
borderColor = "!border-amber-500";
|
|
1847
|
+
}
|
|
1848
|
+
else {
|
|
1849
|
+
borderColor = "!border-gray-500 dark:!border-gray-400";
|
|
1850
|
+
}
|
|
1851
|
+
return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
|
|
1852
|
+
}
|
|
1723
1853
|
|
|
1724
1854
|
const WorkbenchContext = createContext(null);
|
|
1725
1855
|
function useWorkbenchContext() {
|
|
@@ -1736,14 +1866,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1736
1866
|
const clearEvents = useCallback(() => setEvents([]), []);
|
|
1737
1867
|
const [systemErrors, setSystemErrors] = useState([]);
|
|
1738
1868
|
const [registryErrors, setRegistryErrors] = useState([]);
|
|
1869
|
+
const [inputValidationErrors, setInputValidationErrors] = useState([]);
|
|
1739
1870
|
const clearSystemErrors = useCallback(() => setSystemErrors([]), []);
|
|
1740
1871
|
const clearRegistryErrors = useCallback(() => setRegistryErrors([]), []);
|
|
1872
|
+
const clearInputValidationErrors = useCallback(() => setInputValidationErrors([]), []);
|
|
1741
1873
|
const removeSystemError = useCallback((index) => {
|
|
1742
1874
|
setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1743
1875
|
}, []);
|
|
1744
1876
|
const removeRegistryError = useCallback((index) => {
|
|
1745
1877
|
setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1746
1878
|
}, []);
|
|
1879
|
+
const removeInputValidationError = useCallback((index) => {
|
|
1880
|
+
setInputValidationErrors((prev) => prev.filter((_, idx) => idx !== index));
|
|
1881
|
+
}, []);
|
|
1747
1882
|
// Fallback progress animation: drive progress to 100% over ~2 minutes
|
|
1748
1883
|
const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
|
|
1749
1884
|
const [fallbackStarts, setFallbackStarts] = useState({});
|
|
@@ -1799,7 +1934,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1799
1934
|
const out = {};
|
|
1800
1935
|
// Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
|
|
1801
1936
|
for (const n of def.nodes) {
|
|
1802
|
-
const
|
|
1937
|
+
const effectiveHandles = computeEffectiveHandles(n, registry);
|
|
1938
|
+
const outputsDecl = effectiveHandles.outputs;
|
|
1803
1939
|
const handles = Object.keys(outputsDecl);
|
|
1804
1940
|
const cur = {};
|
|
1805
1941
|
for (const h of handles) {
|
|
@@ -1960,10 +2096,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1960
2096
|
const offRunnerValue = runner.on("value", (e) => {
|
|
1961
2097
|
if (e?.io === "input") {
|
|
1962
2098
|
const nodeId = e?.nodeId;
|
|
2099
|
+
const handle = e?.handle;
|
|
1963
2100
|
setNodeStatus((s) => ({
|
|
1964
2101
|
...s,
|
|
1965
2102
|
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
1966
2103
|
}));
|
|
2104
|
+
// Clear validation errors for this input when a valid value is set
|
|
2105
|
+
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
1967
2106
|
}
|
|
1968
2107
|
return add("runner", "value")(e);
|
|
1969
2108
|
});
|
|
@@ -1972,6 +2111,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
1972
2111
|
const nodeError = e;
|
|
1973
2112
|
const registryError = e;
|
|
1974
2113
|
const systemError = e;
|
|
2114
|
+
const inputValidationError = e;
|
|
1975
2115
|
if (edgeError.kind === "edge-convert") {
|
|
1976
2116
|
const edgeId = edgeError.edgeId;
|
|
1977
2117
|
setEdgeStatus((s) => ({
|
|
@@ -2007,6 +2147,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2007
2147
|
return [...prev, registryError];
|
|
2008
2148
|
});
|
|
2009
2149
|
}
|
|
2150
|
+
else if (inputValidationError.kind === "input-validation") {
|
|
2151
|
+
// Track input validation errors for UI display
|
|
2152
|
+
setInputValidationErrors((prev) => {
|
|
2153
|
+
// Avoid duplicates by checking nodeId, handle, and typeId
|
|
2154
|
+
if (prev.some((err) => err.nodeId === inputValidationError.nodeId &&
|
|
2155
|
+
err.handle === inputValidationError.handle &&
|
|
2156
|
+
err.typeId === inputValidationError.typeId)) {
|
|
2157
|
+
return prev;
|
|
2158
|
+
}
|
|
2159
|
+
return [...prev, inputValidationError];
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2010
2162
|
else if (systemError.kind === "system") {
|
|
2011
2163
|
// Track custom errors for UI display
|
|
2012
2164
|
setSystemErrors((prev) => {
|
|
@@ -2130,7 +2282,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2130
2282
|
}
|
|
2131
2283
|
return add("runner", "stats")(s);
|
|
2132
2284
|
});
|
|
2133
|
-
const offWbGraphChanged = wb.on("graphChanged",
|
|
2285
|
+
const offWbGraphChanged = wb.on("graphChanged", (event) => {
|
|
2286
|
+
// Clear validation errors for removed nodes
|
|
2287
|
+
if (event.change?.type === "removeNode") {
|
|
2288
|
+
const removedNodeId = event.change.nodeId;
|
|
2289
|
+
setInputValidationErrors((prev) => prev.filter((err) => err.nodeId !== removedNodeId));
|
|
2290
|
+
}
|
|
2291
|
+
return add("workbench", "graphChanged")(event);
|
|
2292
|
+
});
|
|
2134
2293
|
const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
2135
2294
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
2136
2295
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
@@ -2300,10 +2459,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2300
2459
|
clearEvents,
|
|
2301
2460
|
systemErrors,
|
|
2302
2461
|
registryErrors,
|
|
2462
|
+
inputValidationErrors,
|
|
2303
2463
|
clearSystemErrors,
|
|
2304
2464
|
clearRegistryErrors,
|
|
2465
|
+
clearInputValidationErrors,
|
|
2305
2466
|
removeSystemError,
|
|
2306
2467
|
removeRegistryError,
|
|
2468
|
+
removeInputValidationError,
|
|
2307
2469
|
isRunning,
|
|
2308
2470
|
engineKind,
|
|
2309
2471
|
start,
|
|
@@ -2328,10 +2490,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2328
2490
|
valuesTick,
|
|
2329
2491
|
systemErrors,
|
|
2330
2492
|
registryErrors,
|
|
2493
|
+
inputValidationErrors,
|
|
2331
2494
|
clearSystemErrors,
|
|
2332
2495
|
clearRegistryErrors,
|
|
2496
|
+
clearInputValidationErrors,
|
|
2333
2497
|
removeSystemError,
|
|
2334
2498
|
removeRegistryError,
|
|
2499
|
+
removeInputValidationError,
|
|
2335
2500
|
inputsMap,
|
|
2336
2501
|
inputDefaultsMap,
|
|
2337
2502
|
outputsMap,
|
|
@@ -2407,7 +2572,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
|
|
|
2407
2572
|
return (jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsx("div", { className: "font-semibold", children: "Events" }), jsxs("div", { className: "flex items-center gap-2", children: [jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsx("span", { children: "Hide workbench" })] }), jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsx("span", { children: "Auto scroll" })] }), jsx("button", { onClick: handleCopyLogs, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: copied ? "Copied!" : "Copy logs as formatted JSON", children: jsx(CopyIcon, { size: 14 }) }), jsx("button", { onClick: clearEvents, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: "Clear all events", children: jsx(TrashIcon, { size: 14 }) })] })] }), jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev) => (jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxs("div", { className: "flex items-baseline gap-2", children: [jsx("span", { className: "w-12 shrink-0 text-right text-gray-500 select-none", children: ev.no }), jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsx("pre", { className: "m-0 whitespace-pre-wrap ml-12", children: renderPayload(ev.payload) })] }, `${ev.at}:${ev.no}`))) })] }));
|
|
2408
2573
|
}
|
|
2409
2574
|
|
|
2410
|
-
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString,
|
|
2575
|
+
function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, contextPanel, setInput, }) {
|
|
2411
2576
|
const safeToString = (typeId, value) => {
|
|
2412
2577
|
try {
|
|
2413
2578
|
if (typeof toString === "function") {
|
|
@@ -2419,7 +2584,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2419
2584
|
return String(value ?? "");
|
|
2420
2585
|
}
|
|
2421
2586
|
};
|
|
2422
|
-
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
|
|
2587
|
+
const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
|
|
2423
2588
|
const nodeValidationIssues = validationByNode.issues;
|
|
2424
2589
|
const edgeValidationIssues = validationByEdge.issues;
|
|
2425
2590
|
const nodeValidationHandles = validationByNode;
|
|
@@ -2530,7 +2695,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2530
2695
|
}
|
|
2531
2696
|
catch { }
|
|
2532
2697
|
};
|
|
2533
|
-
return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2698
|
+
return (jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsx("div", { className: "break-words", children: err.message }), jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsx("div", { className: "break-words", children: err.message })] }), jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-semibold", children: "Registry Error" }), jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsx(XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxs("div", { children: [jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxs("li", { className: "flex items-center gap-1", children: [jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
2534
2699
|
e.stopPropagation();
|
|
2535
2700
|
deleteEdgeById(m.data?.edgeId);
|
|
2536
2701
|
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxs("div", { children: [jsxs("div", { className: "mb-2", children: [jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), (() => {
|
|
@@ -2568,13 +2733,20 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
2568
2733
|
const current = nodeInputs[h];
|
|
2569
2734
|
const hasValue = current !== undefined && current !== null;
|
|
2570
2735
|
const value = drafts[h] ?? safeToString(typeId, current);
|
|
2571
|
-
const displayValue =
|
|
2736
|
+
const displayValue = value;
|
|
2572
2737
|
const placeholder = hasDefault ? defaultStr : undefined;
|
|
2573
2738
|
const onChangeText = (text) => setDrafts((d) => ({ ...d, [h]: text }));
|
|
2574
2739
|
const commit = () => {
|
|
2575
2740
|
const draft = drafts[h];
|
|
2576
2741
|
if (draft === undefined)
|
|
2577
2742
|
return;
|
|
2743
|
+
// Only commit if draft differs from current value
|
|
2744
|
+
const currentDisplay = safeToString(typeId, current);
|
|
2745
|
+
if (draft === currentDisplay) {
|
|
2746
|
+
// No change, just sync originals without calling setInput
|
|
2747
|
+
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2578
2750
|
setInput(h, draft);
|
|
2579
2751
|
setOriginals((o) => ({ ...o, [h]: draft }));
|
|
2580
2752
|
};
|
|
@@ -2778,15 +2950,17 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
2778
2950
|
const status = data.status ?? { activeRuns: 0 };
|
|
2779
2951
|
const validation = data.validation ?? {
|
|
2780
2952
|
inputs: [],
|
|
2781
|
-
outputs: []
|
|
2953
|
+
outputs: [],
|
|
2954
|
+
issues: [],
|
|
2955
|
+
};
|
|
2782
2956
|
const isRunning = !!status.activeRuns;
|
|
2783
2957
|
const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
|
|
2784
|
-
return (jsxs(Fragment, { children: [jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
}, renderLabel: ({ kind, id: handleId }) => {
|
|
2958
|
+
return (jsxs(Fragment, { children: [jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => getHandleClassName({
|
|
2959
|
+
kind,
|
|
2960
|
+
id,
|
|
2961
|
+
validation,
|
|
2962
|
+
inputConnected: data.inputConnected,
|
|
2963
|
+
}), renderLabel: ({ kind, id: handleId }) => {
|
|
2790
2964
|
const entries = kind === "input" ? inputEntries : outputEntries;
|
|
2791
2965
|
const entry = entries.find((e) => e.id === handleId);
|
|
2792
2966
|
if (!entry)
|
|
@@ -3392,9 +3566,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3392
3566
|
state: "local",
|
|
3393
3567
|
});
|
|
3394
3568
|
const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
|
|
3395
|
-
const
|
|
3396
|
-
?
|
|
3397
|
-
:
|
|
3569
|
+
const effectiveHandles = selectedNode
|
|
3570
|
+
? computeEffectiveHandles(selectedNode, registry)
|
|
3571
|
+
: { inputs: {}, outputs: {}, inputDefaults: {} };
|
|
3398
3572
|
const [exampleState, setExampleState] = useState(example ?? "");
|
|
3399
3573
|
const defaultExamples = useMemo(() => [
|
|
3400
3574
|
{
|
|
@@ -3682,7 +3856,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3682
3856
|
const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
|
|
3683
3857
|
if (isLinked)
|
|
3684
3858
|
return;
|
|
3685
|
-
|
|
3859
|
+
// If raw is undefined, pass it through to delete the input value
|
|
3860
|
+
if (raw === undefined) {
|
|
3861
|
+
runner.setInput(selectedNodeId, handle, undefined);
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
const typeId = getInputTypeId(effectiveHandles.inputs, handle);
|
|
3686
3865
|
let value = raw;
|
|
3687
3866
|
const parseArray = (s, map) => {
|
|
3688
3867
|
const str = String(s).trim();
|
|
@@ -3759,7 +3938,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3759
3938
|
}
|
|
3760
3939
|
}
|
|
3761
3940
|
runner.setInput(selectedNodeId, handle, value);
|
|
3762
|
-
}, [selectedNodeId, def.edges,
|
|
3941
|
+
}, [selectedNodeId, def.edges, effectiveHandles, runner]);
|
|
3763
3942
|
const setInput = useMemo(() => {
|
|
3764
3943
|
if (overrides?.setInput) {
|
|
3765
3944
|
return overrides.setInput(baseSetInput, {
|
|
@@ -3891,7 +4070,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
|
|
|
3891
4070
|
const message = err instanceof Error ? err.message : String(err);
|
|
3892
4071
|
alert(message);
|
|
3893
4072
|
}
|
|
3894
|
-
}, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString,
|
|
4073
|
+
}, children: jsx(DownloadIcon, { size: 24 }) }), jsx("input", { ref: uploadInputRef, type: "file", accept: "application/json,.json", className: "hidden", onChange: onUploadPicked }), jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: triggerUpload, children: jsx(UploadIcon, { size: 24 }) }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsx(BugBeetleIcon, { size: 24, weight: debug ? "fill" : undefined })] }), jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsx(ListBulletsIcon, { size: 24, weight: showValues ? "fill" : undefined })] })] }), jsxs("div", { className: "flex flex-1 min-h-0", children: [jsx("div", { className: "flex-1 min-w-0", children: jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement, getDefaultNodeSize: overrides?.getDefaultNodeSize }) }), jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, contextPanel: overrides?.contextPanel })] })] }));
|
|
3895
4074
|
}
|
|
3896
4075
|
function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
|
|
3897
4076
|
const [registry, setRegistry] = useState(createSimpleGraphRegistry());
|
|
@@ -3972,5 +4151,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
|
|
|
3972
4151
|
}, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, backendOptions: backendOptions, overrides: overrides, onInit: onInit, onChange: onChange }) }));
|
|
3973
4152
|
}
|
|
3974
4153
|
|
|
3975
|
-
export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, formatDataUrlAsLabel, formatDeclaredTypeSignature, getNodeBorderClassNames, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
|
|
4154
|
+
export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleClassName, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
|
|
3976
4155
|
//# sourceMappingURL=index.js.map
|