@3plate/graph-core 0.1.5 → 0.1.7
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/dist/index.cjs +1216 -42
- package/dist/index.d.cts +224 -4
- package/dist/index.d.ts +224 -4
- package/dist/index.js +1210 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1338,15 +1338,16 @@ var Layout = class _Layout {
|
|
|
1338
1338
|
|
|
1339
1339
|
// src/canvas/marker.tsx
|
|
1340
1340
|
import { jsx } from "jsx-dom/jsx-runtime";
|
|
1341
|
-
function arrow(size, reverse = false) {
|
|
1341
|
+
function arrow(size, reverse = false, prefix = "") {
|
|
1342
1342
|
const h = size / 1.5;
|
|
1343
1343
|
const w = size;
|
|
1344
1344
|
const ry = h / 2;
|
|
1345
1345
|
const suffix = reverse ? "-reverse" : "";
|
|
1346
|
+
const id = prefix ? `${prefix}-g3p-marker-arrow${suffix}` : `g3p-marker-arrow${suffix}`;
|
|
1346
1347
|
return /* @__PURE__ */ jsx(
|
|
1347
1348
|
"marker",
|
|
1348
1349
|
{
|
|
1349
|
-
id
|
|
1350
|
+
id,
|
|
1350
1351
|
className: "g3p-marker g3p-marker-arrow",
|
|
1351
1352
|
markerWidth: size,
|
|
1352
1353
|
markerHeight: size,
|
|
@@ -1358,14 +1359,15 @@ function arrow(size, reverse = false) {
|
|
|
1358
1359
|
}
|
|
1359
1360
|
);
|
|
1360
1361
|
}
|
|
1361
|
-
function circle(size, reverse = false) {
|
|
1362
|
+
function circle(size, reverse = false, prefix = "") {
|
|
1362
1363
|
const r = size / 3;
|
|
1363
1364
|
const cy = size / 2;
|
|
1364
1365
|
const suffix = reverse ? "-reverse" : "";
|
|
1366
|
+
const id = prefix ? `${prefix}-g3p-marker-circle${suffix}` : `g3p-marker-circle${suffix}`;
|
|
1365
1367
|
return /* @__PURE__ */ jsx(
|
|
1366
1368
|
"marker",
|
|
1367
1369
|
{
|
|
1368
|
-
id
|
|
1370
|
+
id,
|
|
1369
1371
|
className: "g3p-marker g3p-marker-circle",
|
|
1370
1372
|
markerWidth: size,
|
|
1371
1373
|
markerHeight: size,
|
|
@@ -1377,15 +1379,16 @@ function circle(size, reverse = false) {
|
|
|
1377
1379
|
}
|
|
1378
1380
|
);
|
|
1379
1381
|
}
|
|
1380
|
-
function diamond(size, reverse = false) {
|
|
1382
|
+
function diamond(size, reverse = false, prefix = "") {
|
|
1381
1383
|
const w = size * 0.7;
|
|
1382
1384
|
const h = size / 2;
|
|
1383
1385
|
const cy = size / 2;
|
|
1384
1386
|
const suffix = reverse ? "-reverse" : "";
|
|
1387
|
+
const id = prefix ? `${prefix}-g3p-marker-diamond${suffix}` : `g3p-marker-diamond${suffix}`;
|
|
1385
1388
|
return /* @__PURE__ */ jsx(
|
|
1386
1389
|
"marker",
|
|
1387
1390
|
{
|
|
1388
|
-
id
|
|
1391
|
+
id,
|
|
1389
1392
|
className: "g3p-marker g3p-marker-diamond",
|
|
1390
1393
|
markerWidth: size,
|
|
1391
1394
|
markerHeight: size,
|
|
@@ -1397,14 +1400,15 @@ function diamond(size, reverse = false) {
|
|
|
1397
1400
|
}
|
|
1398
1401
|
);
|
|
1399
1402
|
}
|
|
1400
|
-
function bar(size, reverse = false) {
|
|
1403
|
+
function bar(size, reverse = false, prefix = "") {
|
|
1401
1404
|
const h = size * 0.6;
|
|
1402
1405
|
const cy = size / 2;
|
|
1403
1406
|
const suffix = reverse ? "-reverse" : "";
|
|
1407
|
+
const id = prefix ? `${prefix}-g3p-marker-bar${suffix}` : `g3p-marker-bar${suffix}`;
|
|
1404
1408
|
return /* @__PURE__ */ jsx(
|
|
1405
1409
|
"marker",
|
|
1406
1410
|
{
|
|
1407
|
-
id
|
|
1411
|
+
id,
|
|
1408
1412
|
className: "g3p-marker g3p-marker-bar",
|
|
1409
1413
|
markerWidth: size,
|
|
1410
1414
|
markerHeight: size,
|
|
@@ -1416,7 +1420,7 @@ function bar(size, reverse = false) {
|
|
|
1416
1420
|
}
|
|
1417
1421
|
);
|
|
1418
1422
|
}
|
|
1419
|
-
function none(size, reverse = false) {
|
|
1423
|
+
function none(size, reverse = false, prefix = "") {
|
|
1420
1424
|
return void 0;
|
|
1421
1425
|
}
|
|
1422
1426
|
function normalize(data) {
|
|
@@ -2198,6 +2202,9 @@ var Seg2 = class {
|
|
|
2198
2202
|
if (this.source.isDummy) source = void 0;
|
|
2199
2203
|
if (this.target.isDummy) target = void 0;
|
|
2200
2204
|
const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
|
|
2205
|
+
const prefix = this.canvas.markerPrefix;
|
|
2206
|
+
const markerStartId = source ? prefix ? `${prefix}-g3p-marker-${source}-reverse` : `g3p-marker-${source}-reverse` : void 0;
|
|
2207
|
+
const markerEndId = target ? prefix ? `${prefix}-g3p-marker-${target}` : `g3p-marker-${target}` : void 0;
|
|
2201
2208
|
return /* @__PURE__ */ jsxs2(
|
|
2202
2209
|
"g",
|
|
2203
2210
|
{
|
|
@@ -2212,8 +2219,8 @@ var Seg2 = class {
|
|
|
2212
2219
|
d: this.svg,
|
|
2213
2220
|
fill: "none",
|
|
2214
2221
|
className: "g3p-seg-line",
|
|
2215
|
-
markerStart:
|
|
2216
|
-
markerEnd:
|
|
2222
|
+
markerStart: markerStartId ? `url(#${markerStartId})` : void 0,
|
|
2223
|
+
markerEnd: markerEndId ? `url(#${markerEndId})` : void 0
|
|
2217
2224
|
}
|
|
2218
2225
|
),
|
|
2219
2226
|
/* @__PURE__ */ jsx3(
|
|
@@ -2798,6 +2805,10 @@ var Canvas = class {
|
|
|
2798
2805
|
curNodes;
|
|
2799
2806
|
curSegs;
|
|
2800
2807
|
updating;
|
|
2808
|
+
// Unique marker ID prefix for this canvas instance
|
|
2809
|
+
markerPrefix;
|
|
2810
|
+
// Dynamic style element for this instance (for cleanup)
|
|
2811
|
+
dynamicStyleEl;
|
|
2801
2812
|
// Pan-zoom state
|
|
2802
2813
|
panScale = null;
|
|
2803
2814
|
zoomControls;
|
|
@@ -2811,6 +2822,13 @@ var Canvas = class {
|
|
|
2811
2822
|
constructor(api, options) {
|
|
2812
2823
|
Object.assign(this, options);
|
|
2813
2824
|
this.api = api;
|
|
2825
|
+
this.reset();
|
|
2826
|
+
this.markerPrefix = api.root.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
2827
|
+
this.createMeasurementContainer();
|
|
2828
|
+
this.createCanvasContainer();
|
|
2829
|
+
if (this.panZoom) this.setupPanZoom();
|
|
2830
|
+
}
|
|
2831
|
+
reset() {
|
|
2814
2832
|
this.allNodes = /* @__PURE__ */ new Map();
|
|
2815
2833
|
this.curNodes = /* @__PURE__ */ new Map();
|
|
2816
2834
|
this.curSegs = /* @__PURE__ */ new Map();
|
|
@@ -2819,9 +2837,7 @@ var Canvas = class {
|
|
|
2819
2837
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2820
2838
|
this.editMode = new EditMode();
|
|
2821
2839
|
this.editMode.editable = this.editable;
|
|
2822
|
-
this.
|
|
2823
|
-
this.createCanvasContainer();
|
|
2824
|
-
if (this.panZoom) this.setupPanZoom();
|
|
2840
|
+
if (this.group) this.group.innerHTML = "";
|
|
2825
2841
|
}
|
|
2826
2842
|
createMeasurementContainer() {
|
|
2827
2843
|
this.measurement = document.createElement("div");
|
|
@@ -3006,12 +3022,13 @@ var Canvas = class {
|
|
|
3006
3022
|
}
|
|
3007
3023
|
generateDynamicStyles() {
|
|
3008
3024
|
let css = "";
|
|
3009
|
-
|
|
3025
|
+
const scope = `[data-g3p-instance="${this.markerPrefix}"]`;
|
|
3026
|
+
css += themeToCSS(this.theme, scope);
|
|
3010
3027
|
for (const [type, vars] of Object.entries(this.nodeTypes)) {
|
|
3011
|
-
css += themeToCSS(vars,
|
|
3028
|
+
css += themeToCSS(vars, `${scope} .g3p-node-type-${type}`, "node");
|
|
3012
3029
|
}
|
|
3013
3030
|
for (const [type, vars] of Object.entries(this.edgeTypes)) {
|
|
3014
|
-
css += themeToCSS(vars,
|
|
3031
|
+
css += themeToCSS(vars, `${scope} .g3p-edge-type-${type}`);
|
|
3015
3032
|
}
|
|
3016
3033
|
return css;
|
|
3017
3034
|
}
|
|
@@ -3024,15 +3041,18 @@ var Canvas = class {
|
|
|
3024
3041
|
}
|
|
3025
3042
|
const dynamicStyles = this.generateDynamicStyles();
|
|
3026
3043
|
if (dynamicStyles) {
|
|
3027
|
-
|
|
3028
|
-
dynamicStyleEl
|
|
3029
|
-
|
|
3044
|
+
this.dynamicStyleEl?.remove();
|
|
3045
|
+
this.dynamicStyleEl = document.createElement("style");
|
|
3046
|
+
this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
|
|
3047
|
+
this.dynamicStyleEl.textContent = dynamicStyles;
|
|
3048
|
+
document.head.appendChild(this.dynamicStyleEl);
|
|
3030
3049
|
}
|
|
3031
3050
|
const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
|
|
3032
3051
|
this.container = /* @__PURE__ */ jsx6(
|
|
3033
3052
|
"div",
|
|
3034
3053
|
{
|
|
3035
3054
|
className: `g3p-canvas-container ${colorModeClass}`.trim(),
|
|
3055
|
+
"data-g3p-instance": this.markerPrefix,
|
|
3036
3056
|
ref: (el) => this.container = el,
|
|
3037
3057
|
onContextMenu: this.onContextMenu.bind(this),
|
|
3038
3058
|
children: /* @__PURE__ */ jsxs5(
|
|
@@ -3048,8 +3068,8 @@ var Canvas = class {
|
|
|
3048
3068
|
onDblClick: this.onDoubleClick.bind(this),
|
|
3049
3069
|
children: [
|
|
3050
3070
|
/* @__PURE__ */ jsxs5("defs", { children: [
|
|
3051
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
|
|
3052
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
|
|
3071
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, false, this.markerPrefix)),
|
|
3072
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, true, this.markerPrefix))
|
|
3053
3073
|
] }),
|
|
3054
3074
|
/* @__PURE__ */ jsx6(
|
|
3055
3075
|
"g",
|
|
@@ -3406,6 +3426,40 @@ var Canvas = class {
|
|
|
3406
3426
|
}
|
|
3407
3427
|
return { type: "canvas" };
|
|
3408
3428
|
}
|
|
3429
|
+
/** Update theme and type styles dynamically */
|
|
3430
|
+
updateStyles(options) {
|
|
3431
|
+
if (options.theme !== void 0) this.theme = options.theme;
|
|
3432
|
+
if (options.nodeTypes !== void 0) this.nodeTypes = options.nodeTypes;
|
|
3433
|
+
if (options.edgeTypes !== void 0) this.edgeTypes = options.edgeTypes;
|
|
3434
|
+
const dynamicStyles = this.generateDynamicStyles();
|
|
3435
|
+
if (dynamicStyles) {
|
|
3436
|
+
if (!this.dynamicStyleEl) {
|
|
3437
|
+
this.dynamicStyleEl = document.createElement("style");
|
|
3438
|
+
this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
|
|
3439
|
+
document.head.appendChild(this.dynamicStyleEl);
|
|
3440
|
+
}
|
|
3441
|
+
this.dynamicStyleEl.textContent = dynamicStyles;
|
|
3442
|
+
} else if (this.dynamicStyleEl) {
|
|
3443
|
+
this.dynamicStyleEl.remove();
|
|
3444
|
+
this.dynamicStyleEl = void 0;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
/** Update color mode without recreating the canvas */
|
|
3448
|
+
setColorMode(colorMode) {
|
|
3449
|
+
if (!this.container) return;
|
|
3450
|
+
this.colorMode = colorMode;
|
|
3451
|
+
this.container.classList.remove("g3p-light", "g3p-dark");
|
|
3452
|
+
if (colorMode !== "system") {
|
|
3453
|
+
this.container.classList.add(`g3p-${colorMode}`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
/** Cleanup resources when the canvas is destroyed */
|
|
3457
|
+
destroy() {
|
|
3458
|
+
this.dynamicStyleEl?.remove();
|
|
3459
|
+
this.dynamicStyleEl = void 0;
|
|
3460
|
+
this.measurement?.remove();
|
|
3461
|
+
this.measurement = void 0;
|
|
3462
|
+
}
|
|
3409
3463
|
};
|
|
3410
3464
|
var themeVarMap = {
|
|
3411
3465
|
// Canvas
|
|
@@ -3520,26 +3574,50 @@ var Updater = class _Updater {
|
|
|
3520
3574
|
this.update.addNodes.push(node);
|
|
3521
3575
|
return this;
|
|
3522
3576
|
}
|
|
3577
|
+
addNodes(...nodes) {
|
|
3578
|
+
this.update.addNodes.push(...nodes);
|
|
3579
|
+
return this;
|
|
3580
|
+
}
|
|
3523
3581
|
deleteNode(node) {
|
|
3524
3582
|
this.update.removeNodes.push(node);
|
|
3525
3583
|
return this;
|
|
3526
3584
|
}
|
|
3585
|
+
deleteNodes(...nodes) {
|
|
3586
|
+
this.update.removeNodes.push(...nodes);
|
|
3587
|
+
return this;
|
|
3588
|
+
}
|
|
3527
3589
|
updateNode(node) {
|
|
3528
3590
|
this.update.updateNodes.push(node);
|
|
3529
3591
|
return this;
|
|
3530
3592
|
}
|
|
3593
|
+
updateNodes(...nodes) {
|
|
3594
|
+
this.update.updateNodes.push(...nodes);
|
|
3595
|
+
return this;
|
|
3596
|
+
}
|
|
3531
3597
|
addEdge(edge) {
|
|
3532
3598
|
this.update.addEdges.push(edge);
|
|
3533
3599
|
return this;
|
|
3534
3600
|
}
|
|
3601
|
+
addEdges(...edges) {
|
|
3602
|
+
this.update.addEdges.push(...edges);
|
|
3603
|
+
return this;
|
|
3604
|
+
}
|
|
3535
3605
|
deleteEdge(edge) {
|
|
3536
3606
|
this.update.removeEdges.push(edge);
|
|
3537
3607
|
return this;
|
|
3538
3608
|
}
|
|
3609
|
+
deleteEdges(...edges) {
|
|
3610
|
+
this.update.removeEdges.push(...edges);
|
|
3611
|
+
return this;
|
|
3612
|
+
}
|
|
3539
3613
|
updateEdge(edge) {
|
|
3540
3614
|
this.update.updateEdges.push(edge);
|
|
3541
3615
|
return this;
|
|
3542
3616
|
}
|
|
3617
|
+
updateEdges(...edges) {
|
|
3618
|
+
this.update.updateEdges.push(...edges);
|
|
3619
|
+
return this;
|
|
3620
|
+
}
|
|
3543
3621
|
static add(nodes, edges) {
|
|
3544
3622
|
const updater = new _Updater();
|
|
3545
3623
|
updater.update.addNodes = nodes;
|
|
@@ -3570,19 +3648,8 @@ var API = class {
|
|
|
3570
3648
|
constructor(args) {
|
|
3571
3649
|
this.root = args.root;
|
|
3572
3650
|
this.options = applyDefaults(args.options);
|
|
3573
|
-
let graph2 = new Graph({ options: this.options.graph });
|
|
3574
|
-
this.state = { graph: graph2, update: null };
|
|
3575
3651
|
this.events = args.events || {};
|
|
3576
|
-
this.
|
|
3577
|
-
this.index = 0;
|
|
3578
|
-
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3579
|
-
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3580
|
-
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3581
|
-
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3582
|
-
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3583
|
-
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3584
|
-
this.nextNodeId = 1;
|
|
3585
|
-
this.nextEdgeId = 1;
|
|
3652
|
+
this.reset();
|
|
3586
3653
|
this.canvas = new Canvas(this, {
|
|
3587
3654
|
...this.options.canvas,
|
|
3588
3655
|
dummyNodeSize: this.options.graph.dummyNodeSize,
|
|
@@ -3596,6 +3663,35 @@ var API = class {
|
|
|
3596
3663
|
this.history = [];
|
|
3597
3664
|
}
|
|
3598
3665
|
}
|
|
3666
|
+
reset() {
|
|
3667
|
+
let graph2 = new Graph({ options: this.options.graph });
|
|
3668
|
+
this.state = { graph: graph2, update: null };
|
|
3669
|
+
this.seq = [this.state];
|
|
3670
|
+
this.index = 0;
|
|
3671
|
+
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3672
|
+
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3673
|
+
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3674
|
+
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3675
|
+
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3676
|
+
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3677
|
+
this.nextNodeId = 1;
|
|
3678
|
+
this.nextEdgeId = 1;
|
|
3679
|
+
this.canvas?.reset?.();
|
|
3680
|
+
}
|
|
3681
|
+
/** Initialize the API */
|
|
3682
|
+
async init() {
|
|
3683
|
+
const root = document.getElementById(this.root);
|
|
3684
|
+
if (!root) throw new Error("root element not found");
|
|
3685
|
+
root.appendChild(this.canvas.container);
|
|
3686
|
+
await this.applyHistory();
|
|
3687
|
+
if (this.events.onInit) {
|
|
3688
|
+
this.events.onInit();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
async applyHistory() {
|
|
3692
|
+
for (const update of this.history)
|
|
3693
|
+
await this.applyUpdate(update);
|
|
3694
|
+
}
|
|
3599
3695
|
/** Current history index (0-based) */
|
|
3600
3696
|
getHistoryIndex() {
|
|
3601
3697
|
return this.index;
|
|
@@ -3608,17 +3704,25 @@ var API = class {
|
|
|
3608
3704
|
setEditable(editable) {
|
|
3609
3705
|
this.canvas.editMode.editable = editable;
|
|
3610
3706
|
}
|
|
3707
|
+
/** Replace entire history (clears prior) */
|
|
3708
|
+
async replaceHistory(frames) {
|
|
3709
|
+
this.reset();
|
|
3710
|
+
this.history = frames;
|
|
3711
|
+
await this.applyHistory();
|
|
3712
|
+
}
|
|
3713
|
+
/** Rebuild from snapshot (nodes/edges) */
|
|
3714
|
+
async replaceSnapshot(nodes, edges, description) {
|
|
3715
|
+
this.reset();
|
|
3716
|
+
this.history = [{
|
|
3717
|
+
addNodes: nodes,
|
|
3718
|
+
addEdges: edges,
|
|
3719
|
+
description
|
|
3720
|
+
}];
|
|
3721
|
+
await this.applyHistory();
|
|
3722
|
+
}
|
|
3611
3723
|
get graph() {
|
|
3612
3724
|
return this.state.graph;
|
|
3613
3725
|
}
|
|
3614
|
-
/** Initialize the API */
|
|
3615
|
-
async init() {
|
|
3616
|
-
const root = document.getElementById(this.root);
|
|
3617
|
-
if (!root) throw new Error("root element not found");
|
|
3618
|
-
root.appendChild(this.canvas.container);
|
|
3619
|
-
for (const update of this.history)
|
|
3620
|
-
await this.applyUpdate(update);
|
|
3621
|
-
}
|
|
3622
3726
|
/** Navigate to a different state */
|
|
3623
3727
|
nav(nav) {
|
|
3624
3728
|
let newIndex;
|
|
@@ -4038,6 +4142,1064 @@ var API = class {
|
|
|
4038
4142
|
else
|
|
4039
4143
|
await this.deleteEdge(edge.data);
|
|
4040
4144
|
}
|
|
4145
|
+
/** Update theme and type styles dynamically */
|
|
4146
|
+
updateStyles(options) {
|
|
4147
|
+
this.canvas?.updateStyles(options);
|
|
4148
|
+
}
|
|
4149
|
+
/** Update color mode without recreating the canvas */
|
|
4150
|
+
setColorMode(colorMode) {
|
|
4151
|
+
this.canvas?.setColorMode(colorMode);
|
|
4152
|
+
}
|
|
4153
|
+
/** Cleanup resources when the graph is destroyed */
|
|
4154
|
+
destroy() {
|
|
4155
|
+
this.canvas?.destroy();
|
|
4156
|
+
}
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4159
|
+
// src/api/ingest.ts
|
|
4160
|
+
var Ingest = class {
|
|
4161
|
+
constructor(api) {
|
|
4162
|
+
this.api = api;
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Apply an incoming ingest message to the API.
|
|
4166
|
+
* - snapshot: rebuild state from nodes/edges (clears prior history)
|
|
4167
|
+
* - update: apply incremental update
|
|
4168
|
+
* - history: initialize from a set of frames (clears prior history)
|
|
4169
|
+
*/
|
|
4170
|
+
async apply(msg) {
|
|
4171
|
+
switch (msg.type) {
|
|
4172
|
+
case "snapshot": {
|
|
4173
|
+
await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
|
|
4174
|
+
break;
|
|
4175
|
+
}
|
|
4176
|
+
case "update": {
|
|
4177
|
+
await this.api.update((u) => {
|
|
4178
|
+
if (msg.addNodes) u.addNodes(...msg.addNodes);
|
|
4179
|
+
if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
|
|
4180
|
+
if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
|
|
4181
|
+
if (msg.addEdges) u.addEdges(...msg.addEdges);
|
|
4182
|
+
if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
|
|
4183
|
+
if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
|
|
4184
|
+
if (msg.description) u.describe(msg.description);
|
|
4185
|
+
});
|
|
4186
|
+
break;
|
|
4187
|
+
}
|
|
4188
|
+
case "history": {
|
|
4189
|
+
await this.api.replaceHistory(msg.frames);
|
|
4190
|
+
break;
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
|
|
4196
|
+
// src/api/sources/WebSocketSource.ts
|
|
4197
|
+
var WebSocketSource = class {
|
|
4198
|
+
url;
|
|
4199
|
+
ws = null;
|
|
4200
|
+
onMessage;
|
|
4201
|
+
onStatus;
|
|
4202
|
+
reconnectMs;
|
|
4203
|
+
closedByUser = false;
|
|
4204
|
+
connectStartTime = null;
|
|
4205
|
+
totalTimeoutMs = 1e4;
|
|
4206
|
+
totalTimeoutTimer = null;
|
|
4207
|
+
constructor(url, onMessage, onStatus, reconnectMs = 1500) {
|
|
4208
|
+
this.url = url;
|
|
4209
|
+
this.onMessage = onMessage;
|
|
4210
|
+
this.onStatus = onStatus;
|
|
4211
|
+
this.reconnectMs = reconnectMs;
|
|
4212
|
+
}
|
|
4213
|
+
connect() {
|
|
4214
|
+
this.closedByUser = false;
|
|
4215
|
+
this.connectStartTime = Date.now();
|
|
4216
|
+
this.startTotalTimeout();
|
|
4217
|
+
this.open();
|
|
4218
|
+
}
|
|
4219
|
+
disconnect() {
|
|
4220
|
+
this.closedByUser = true;
|
|
4221
|
+
this.clearTotalTimeout();
|
|
4222
|
+
if (this.ws) {
|
|
4223
|
+
try {
|
|
4224
|
+
this.ws.close();
|
|
4225
|
+
} catch {
|
|
4226
|
+
}
|
|
4227
|
+
this.ws = null;
|
|
4228
|
+
}
|
|
4229
|
+
this.onStatus?.("closed");
|
|
4230
|
+
}
|
|
4231
|
+
startTotalTimeout() {
|
|
4232
|
+
this.clearTotalTimeout();
|
|
4233
|
+
this.totalTimeoutTimer = window.setTimeout(() => {
|
|
4234
|
+
if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
|
|
4235
|
+
this.closedByUser = true;
|
|
4236
|
+
if (this.ws) {
|
|
4237
|
+
try {
|
|
4238
|
+
this.ws.close();
|
|
4239
|
+
} catch {
|
|
4240
|
+
}
|
|
4241
|
+
this.ws = null;
|
|
4242
|
+
}
|
|
4243
|
+
this.clearTotalTimeout();
|
|
4244
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4245
|
+
}
|
|
4246
|
+
}, this.totalTimeoutMs);
|
|
4247
|
+
}
|
|
4248
|
+
clearTotalTimeout() {
|
|
4249
|
+
if (this.totalTimeoutTimer !== null) {
|
|
4250
|
+
clearTimeout(this.totalTimeoutTimer);
|
|
4251
|
+
this.totalTimeoutTimer = null;
|
|
4252
|
+
}
|
|
4253
|
+
this.connectStartTime = null;
|
|
4254
|
+
}
|
|
4255
|
+
open() {
|
|
4256
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4257
|
+
if (!this.closedByUser) {
|
|
4258
|
+
this.closedByUser = true;
|
|
4259
|
+
this.clearTotalTimeout();
|
|
4260
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4261
|
+
}
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
this.onStatus?.(this.ws ? "reconnecting" : "connecting");
|
|
4265
|
+
const ws = new WebSocket(this.url);
|
|
4266
|
+
this.ws = ws;
|
|
4267
|
+
ws.onopen = () => {
|
|
4268
|
+
this.clearTotalTimeout();
|
|
4269
|
+
this.onStatus?.("connected");
|
|
4270
|
+
};
|
|
4271
|
+
ws.onerror = (e) => {
|
|
4272
|
+
this.onStatus?.("error", e);
|
|
4273
|
+
};
|
|
4274
|
+
ws.onclose = () => {
|
|
4275
|
+
if (this.closedByUser) {
|
|
4276
|
+
this.onStatus?.("closed");
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4280
|
+
this.closedByUser = true;
|
|
4281
|
+
this.clearTotalTimeout();
|
|
4282
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
this.onStatus?.("reconnecting");
|
|
4286
|
+
setTimeout(() => this.open(), this.reconnectMs);
|
|
4287
|
+
};
|
|
4288
|
+
ws.onmessage = (ev) => {
|
|
4289
|
+
const data = typeof ev.data === "string" ? ev.data : "";
|
|
4290
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4291
|
+
for (const line of lines) {
|
|
4292
|
+
try {
|
|
4293
|
+
const obj = JSON.parse(line);
|
|
4294
|
+
this.onMessage(obj);
|
|
4295
|
+
} catch {
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
};
|
|
4301
|
+
|
|
4302
|
+
// src/api/sources/FileSystemSource.ts
|
|
4303
|
+
var FileSystemSource = class {
|
|
4304
|
+
handle = null;
|
|
4305
|
+
onMessage;
|
|
4306
|
+
onStatus;
|
|
4307
|
+
timer = null;
|
|
4308
|
+
lastSize = 0;
|
|
4309
|
+
filename;
|
|
4310
|
+
intervalMs;
|
|
4311
|
+
constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
|
|
4312
|
+
this.onMessage = onMessage;
|
|
4313
|
+
this.onStatus = onStatus;
|
|
4314
|
+
this.filename = filename;
|
|
4315
|
+
this.intervalMs = intervalMs;
|
|
4316
|
+
}
|
|
4317
|
+
async openDirectory() {
|
|
4318
|
+
try {
|
|
4319
|
+
const dir = await window.showDirectoryPicker?.();
|
|
4320
|
+
if (!dir) throw new Error("File System Access not supported or cancelled");
|
|
4321
|
+
const handle = await dir.getFileHandle(this.filename, { create: false });
|
|
4322
|
+
this.handle = handle;
|
|
4323
|
+
this.onStatus?.("opened", { file: this.filename });
|
|
4324
|
+
this.lastSize = 0;
|
|
4325
|
+
this.startPolling();
|
|
4326
|
+
} catch (e) {
|
|
4327
|
+
this.onStatus?.("error", e);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
close() {
|
|
4331
|
+
if (this.timer) {
|
|
4332
|
+
window.clearInterval(this.timer);
|
|
4333
|
+
this.timer = null;
|
|
4334
|
+
}
|
|
4335
|
+
this.handle = null;
|
|
4336
|
+
this.onStatus?.("closed");
|
|
4337
|
+
}
|
|
4338
|
+
startPolling() {
|
|
4339
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4340
|
+
this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
|
|
4341
|
+
}
|
|
4342
|
+
async readNewLines() {
|
|
4343
|
+
try {
|
|
4344
|
+
if (!this.handle) return;
|
|
4345
|
+
this.onStatus?.("reading");
|
|
4346
|
+
const file = await this.handle.getFile();
|
|
4347
|
+
if (file.size === this.lastSize) return;
|
|
4348
|
+
const slice = await file.slice(this.lastSize).text();
|
|
4349
|
+
this.lastSize = file.size;
|
|
4350
|
+
const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4351
|
+
for (const line of lines) {
|
|
4352
|
+
try {
|
|
4353
|
+
const obj = JSON.parse(line);
|
|
4354
|
+
this.onMessage(obj);
|
|
4355
|
+
} catch {
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
} catch (e) {
|
|
4359
|
+
this.onStatus?.("error", e);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
};
|
|
4363
|
+
|
|
4364
|
+
// src/api/sources/FileSource.ts
|
|
4365
|
+
var FileSource = class {
|
|
4366
|
+
url;
|
|
4367
|
+
onMessage;
|
|
4368
|
+
onStatus;
|
|
4369
|
+
timer = null;
|
|
4370
|
+
lastETag = null;
|
|
4371
|
+
lastContent = "";
|
|
4372
|
+
intervalMs = 1e3;
|
|
4373
|
+
closed = false;
|
|
4374
|
+
constructor(url, onMessage, onStatus, intervalMs = 1e3) {
|
|
4375
|
+
this.url = url;
|
|
4376
|
+
this.onMessage = onMessage;
|
|
4377
|
+
this.onStatus = onStatus;
|
|
4378
|
+
this.intervalMs = intervalMs;
|
|
4379
|
+
}
|
|
4380
|
+
async connect() {
|
|
4381
|
+
this.closed = false;
|
|
4382
|
+
this.lastETag = null;
|
|
4383
|
+
this.lastContent = "";
|
|
4384
|
+
this.onStatus?.("opened");
|
|
4385
|
+
this.startPolling();
|
|
4386
|
+
}
|
|
4387
|
+
close() {
|
|
4388
|
+
this.closed = true;
|
|
4389
|
+
if (this.timer) {
|
|
4390
|
+
window.clearInterval(this.timer);
|
|
4391
|
+
this.timer = null;
|
|
4392
|
+
}
|
|
4393
|
+
this.onStatus?.("closed");
|
|
4394
|
+
}
|
|
4395
|
+
startPolling() {
|
|
4396
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4397
|
+
this.timer = window.setInterval(() => this.poll(), this.intervalMs);
|
|
4398
|
+
this.poll();
|
|
4399
|
+
}
|
|
4400
|
+
async poll() {
|
|
4401
|
+
if (this.closed) return;
|
|
4402
|
+
try {
|
|
4403
|
+
this.onStatus?.("reading");
|
|
4404
|
+
const headers = {};
|
|
4405
|
+
if (this.lastETag) {
|
|
4406
|
+
headers["If-None-Match"] = this.lastETag;
|
|
4407
|
+
}
|
|
4408
|
+
const response = await fetch(this.url, { headers });
|
|
4409
|
+
if (response.status === 304) {
|
|
4410
|
+
return;
|
|
4411
|
+
}
|
|
4412
|
+
if (!response.ok) {
|
|
4413
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4414
|
+
}
|
|
4415
|
+
const etag = response.headers.get("ETag");
|
|
4416
|
+
if (etag) {
|
|
4417
|
+
this.lastETag = etag;
|
|
4418
|
+
}
|
|
4419
|
+
const content = await response.text();
|
|
4420
|
+
if (content === this.lastContent) {
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4424
|
+
const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4425
|
+
const newLines = lines.slice(lastContentLines.length);
|
|
4426
|
+
for (const line of newLines) {
|
|
4427
|
+
try {
|
|
4428
|
+
const obj = JSON.parse(line);
|
|
4429
|
+
this.onMessage(obj);
|
|
4430
|
+
} catch {
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
this.lastContent = content;
|
|
4434
|
+
} catch (e) {
|
|
4435
|
+
this.onStatus?.("error", e);
|
|
4436
|
+
}
|
|
4437
|
+
}
|
|
4438
|
+
};
|
|
4439
|
+
|
|
4440
|
+
// src/playground/playground.ts
|
|
4441
|
+
import styles2 from "./styles.css?raw";
|
|
4442
|
+
var Playground = class {
|
|
4443
|
+
options;
|
|
4444
|
+
rootElement;
|
|
4445
|
+
currentExample;
|
|
4446
|
+
examples;
|
|
4447
|
+
currentGraph = null;
|
|
4448
|
+
ingest = null;
|
|
4449
|
+
isEditable = false;
|
|
4450
|
+
wsSource = null;
|
|
4451
|
+
fsSource = null;
|
|
4452
|
+
fileSource = null;
|
|
4453
|
+
wsStatus = "disconnected";
|
|
4454
|
+
fsStatus = "disconnected";
|
|
4455
|
+
fileStatus = "disconnected";
|
|
4456
|
+
activeSourceType = null;
|
|
4457
|
+
wsUrl = "ws://localhost:8787";
|
|
4458
|
+
sourceModal = null;
|
|
4459
|
+
helpOverlay = null;
|
|
4460
|
+
exampleList;
|
|
4461
|
+
graphContainerId;
|
|
4462
|
+
constructor(options) {
|
|
4463
|
+
this.options = options;
|
|
4464
|
+
this.examples = { ...options.examples };
|
|
4465
|
+
this.exampleList = Object.keys(this.examples);
|
|
4466
|
+
this.currentExample = options.defaultExample || this.exampleList[0];
|
|
4467
|
+
this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
|
|
4468
|
+
if (typeof options.root === "string") {
|
|
4469
|
+
const el = document.getElementById(options.root);
|
|
4470
|
+
if (!el) throw new Error(`Element with id "${options.root}" not found`);
|
|
4471
|
+
this.rootElement = el;
|
|
4472
|
+
} else {
|
|
4473
|
+
this.rootElement = options.root;
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
async init() {
|
|
4477
|
+
this.injectStyles();
|
|
4478
|
+
this.createDOM();
|
|
4479
|
+
this.setupEventListeners();
|
|
4480
|
+
await this.renderGraph();
|
|
4481
|
+
this.updateSourceIcon();
|
|
4482
|
+
this.connectExampleSource();
|
|
4483
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4484
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4485
|
+
}
|
|
4486
|
+
injectStyles() {
|
|
4487
|
+
if (!document.getElementById("g3p-playground-styles")) {
|
|
4488
|
+
const styleEl = document.createElement("style");
|
|
4489
|
+
styleEl.id = "g3p-playground-styles";
|
|
4490
|
+
styleEl.textContent = styles2;
|
|
4491
|
+
document.head.appendChild(styleEl);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
createDOM() {
|
|
4495
|
+
const exampleList = this.exampleList.map((key, i) => {
|
|
4496
|
+
const example = this.examples[key];
|
|
4497
|
+
const isActive = i === 0 || key === this.currentExample;
|
|
4498
|
+
return `
|
|
4499
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
4500
|
+
${example.name}
|
|
4501
|
+
</button>
|
|
4502
|
+
`;
|
|
4503
|
+
}).join("");
|
|
4504
|
+
this.rootElement.innerHTML = `
|
|
4505
|
+
<main class="playground">
|
|
4506
|
+
<div class="sidebar">
|
|
4507
|
+
<h2>Examples</h2>
|
|
4508
|
+
<div class="example-list">
|
|
4509
|
+
${exampleList}
|
|
4510
|
+
</div>
|
|
4511
|
+
|
|
4512
|
+
<h2>Options</h2>
|
|
4513
|
+
<div class="options">
|
|
4514
|
+
<div class="option-group">
|
|
4515
|
+
<label>Orientation</label>
|
|
4516
|
+
<select id="orientation">
|
|
4517
|
+
<option value="TB">Top to Bottom</option>
|
|
4518
|
+
<option value="BT">Bottom to Top</option>
|
|
4519
|
+
<option value="LR">Left to Right</option>
|
|
4520
|
+
<option value="RL">Right to Left</option>
|
|
4521
|
+
</select>
|
|
4522
|
+
</div>
|
|
4523
|
+
|
|
4524
|
+
<div class="option-group">
|
|
4525
|
+
<label>Port Style</label>
|
|
4526
|
+
<select id="portStyle">
|
|
4527
|
+
<option value="outside">Outside</option>
|
|
4528
|
+
<option value="inside">Inside</option>
|
|
4529
|
+
</select>
|
|
4530
|
+
</div>
|
|
4531
|
+
|
|
4532
|
+
<div class="option-group">
|
|
4533
|
+
<label>
|
|
4534
|
+
<input type="checkbox" id="portLabelRotate" />
|
|
4535
|
+
Rotate Port Labels
|
|
4536
|
+
</label>
|
|
4537
|
+
</div>
|
|
4538
|
+
|
|
4539
|
+
<div class="option-group">
|
|
4540
|
+
<label>Theme</label>
|
|
4541
|
+
<select id="colorMode">
|
|
4542
|
+
<option value="system">System</option>
|
|
4543
|
+
<option value="light">Light</option>
|
|
4544
|
+
<option value="dark">Dark</option>
|
|
4545
|
+
</select>
|
|
4546
|
+
</div>
|
|
4547
|
+
</div>
|
|
4548
|
+
</div>
|
|
4549
|
+
|
|
4550
|
+
<div class="graph-area">
|
|
4551
|
+
<div class="graph-toolbar">
|
|
4552
|
+
<div class="nav-controls">
|
|
4553
|
+
<button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
|
|
4554
|
+
<button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
|
|
4555
|
+
<span id="history-label" style="min-width: 4rem; text-align: center; display: inline-flex; align-items: center; justify-content: center; height: 2.25rem;">\u2014 / \u2014</span>
|
|
4556
|
+
<button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
|
|
4557
|
+
<button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
|
|
4558
|
+
</div>
|
|
4559
|
+
<div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
|
|
4560
|
+
<button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
|
|
4561
|
+
</div>
|
|
4562
|
+
<button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
|
|
4563
|
+
<button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
|
|
4564
|
+
<button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
|
|
4565
|
+
</div>
|
|
4566
|
+
<div class="graph-container" id="${this.graphContainerId}"></div>
|
|
4567
|
+
</div>
|
|
4568
|
+
</main>
|
|
4569
|
+
`;
|
|
4570
|
+
}
|
|
4571
|
+
setupEventListeners() {
|
|
4572
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
|
|
4573
|
+
btn.addEventListener("click", () => {
|
|
4574
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
4575
|
+
btn.classList.add("active");
|
|
4576
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
4577
|
+
this.renderGraph();
|
|
4578
|
+
this.connectExampleSource();
|
|
4579
|
+
});
|
|
4580
|
+
});
|
|
4581
|
+
this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
|
|
4582
|
+
el.addEventListener("change", () => {
|
|
4583
|
+
if (el.id === "colorMode" && this.currentGraph) {
|
|
4584
|
+
const mode = el.value;
|
|
4585
|
+
this.currentGraph.setColorMode(mode);
|
|
4586
|
+
} else {
|
|
4587
|
+
this.renderGraph();
|
|
4588
|
+
}
|
|
4589
|
+
});
|
|
4590
|
+
});
|
|
4591
|
+
this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
|
|
4592
|
+
this.currentGraph?.nav("first");
|
|
4593
|
+
this.updateHistoryLabel();
|
|
4594
|
+
});
|
|
4595
|
+
this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
|
|
4596
|
+
this.currentGraph?.nav("prev");
|
|
4597
|
+
this.updateHistoryLabel();
|
|
4598
|
+
});
|
|
4599
|
+
this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
|
|
4600
|
+
this.currentGraph?.nav("next");
|
|
4601
|
+
this.updateHistoryLabel();
|
|
4602
|
+
});
|
|
4603
|
+
this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
|
|
4604
|
+
this.currentGraph?.nav("last");
|
|
4605
|
+
this.updateHistoryLabel();
|
|
4606
|
+
});
|
|
4607
|
+
this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
|
|
4608
|
+
this.currentGraph?.rebuild();
|
|
4609
|
+
});
|
|
4610
|
+
this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
|
|
4611
|
+
this.isEditable = !this.isEditable;
|
|
4612
|
+
const btn = this.rootElement.querySelector("#edit-toggle");
|
|
4613
|
+
if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
|
|
4614
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4615
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4616
|
+
try {
|
|
4617
|
+
this.currentGraph?.setEditable?.(this.isEditable);
|
|
4618
|
+
} catch {
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4621
|
+
this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
|
|
4622
|
+
const sourceIconBtn = this.rootElement.querySelector("#source-icon");
|
|
4623
|
+
if (sourceIconBtn) {
|
|
4624
|
+
sourceIconBtn.addEventListener("click", (e) => {
|
|
4625
|
+
e.preventDefault();
|
|
4626
|
+
e.stopPropagation();
|
|
4627
|
+
this.openSourceModal();
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
document.addEventListener("keydown", (e) => {
|
|
4631
|
+
if (!this.currentGraph) return;
|
|
4632
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
|
|
4633
|
+
switch (e.key) {
|
|
4634
|
+
case "Home":
|
|
4635
|
+
this.currentGraph.nav("first");
|
|
4636
|
+
this.updateHistoryLabel();
|
|
4637
|
+
break;
|
|
4638
|
+
case "End":
|
|
4639
|
+
this.currentGraph.nav("last");
|
|
4640
|
+
this.updateHistoryLabel();
|
|
4641
|
+
break;
|
|
4642
|
+
case "ArrowLeft":
|
|
4643
|
+
this.currentGraph.nav("prev");
|
|
4644
|
+
this.updateHistoryLabel();
|
|
4645
|
+
break;
|
|
4646
|
+
case "ArrowRight":
|
|
4647
|
+
this.currentGraph.nav("next");
|
|
4648
|
+
this.updateHistoryLabel();
|
|
4649
|
+
break;
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
getResolvedColorMode() {
|
|
4654
|
+
const mode = this.rootElement.querySelector("#colorMode")?.value;
|
|
4655
|
+
if (mode === "system") {
|
|
4656
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4657
|
+
}
|
|
4658
|
+
return mode;
|
|
4659
|
+
}
|
|
4660
|
+
getOptions(exampleOptions) {
|
|
4661
|
+
const orientation = this.rootElement.querySelector("#orientation")?.value;
|
|
4662
|
+
return {
|
|
4663
|
+
graph: { orientation },
|
|
4664
|
+
canvas: {
|
|
4665
|
+
width: "100%",
|
|
4666
|
+
height: "100%",
|
|
4667
|
+
colorMode: this.getResolvedColorMode(),
|
|
4668
|
+
editable: this.isEditable,
|
|
4669
|
+
...exampleOptions?.canvas
|
|
4670
|
+
}
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
async renderGraph() {
|
|
4674
|
+
const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
|
|
4675
|
+
if (!container) return;
|
|
4676
|
+
this.currentGraph?.destroy();
|
|
4677
|
+
this.currentGraph = null;
|
|
4678
|
+
container.innerHTML = "";
|
|
4679
|
+
const example = this.examples[this.currentExample];
|
|
4680
|
+
const options = this.getOptions(example.options);
|
|
4681
|
+
try {
|
|
4682
|
+
this.currentGraph = await graph({
|
|
4683
|
+
root: this.graphContainerId,
|
|
4684
|
+
nodes: example.nodes,
|
|
4685
|
+
edges: example.edges,
|
|
4686
|
+
options,
|
|
4687
|
+
events: {
|
|
4688
|
+
historyChange: () => this.updateHistoryLabel()
|
|
4689
|
+
}
|
|
4690
|
+
});
|
|
4691
|
+
this.ingest = new Ingest(this.currentGraph);
|
|
4692
|
+
this.updateHistoryLabel();
|
|
4693
|
+
} catch (e) {
|
|
4694
|
+
console.error("Failed to render graph:", e);
|
|
4695
|
+
container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
updateHistoryLabel() {
|
|
4699
|
+
const label = this.rootElement.querySelector("#history-label");
|
|
4700
|
+
if (!label || !this.currentGraph) return;
|
|
4701
|
+
try {
|
|
4702
|
+
const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
|
|
4703
|
+
const len = this.currentGraph.getHistoryLength?.() ?? 1;
|
|
4704
|
+
label.textContent = `${idx + 1} / ${len}`;
|
|
4705
|
+
} catch {
|
|
4706
|
+
label.textContent = "\u2014 / \u2014";
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
connectExampleSource() {
|
|
4710
|
+
const example = this.examples[this.currentExample];
|
|
4711
|
+
if (!example.source) {
|
|
4712
|
+
this.disconnectAllSources();
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
this.disconnectAllSources();
|
|
4716
|
+
if (example.source.type === "websocket") {
|
|
4717
|
+
this.wsUrl = example.source.url;
|
|
4718
|
+
this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
4719
|
+
this.wsSource.connect();
|
|
4720
|
+
} else if (example.source.type === "file") {
|
|
4721
|
+
this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
|
|
4722
|
+
this.fileSource.connect();
|
|
4723
|
+
}
|
|
4724
|
+
}
|
|
4725
|
+
disconnectAllSources() {
|
|
4726
|
+
this.wsSource?.disconnect();
|
|
4727
|
+
this.fsSource?.close();
|
|
4728
|
+
this.fileSource?.close();
|
|
4729
|
+
this.wsSource = null;
|
|
4730
|
+
this.fsSource = null;
|
|
4731
|
+
this.fileSource = null;
|
|
4732
|
+
this.activeSourceType = null;
|
|
4733
|
+
this.wsStatus = "disconnected";
|
|
4734
|
+
this.fsStatus = "disconnected";
|
|
4735
|
+
this.fileStatus = "disconnected";
|
|
4736
|
+
this.updateSourceIcon();
|
|
4737
|
+
}
|
|
4738
|
+
openHelp() {
|
|
4739
|
+
if (!this.helpOverlay) {
|
|
4740
|
+
this.helpOverlay = document.createElement("div");
|
|
4741
|
+
this.helpOverlay.className = "modal-overlay";
|
|
4742
|
+
this.helpOverlay.innerHTML = `
|
|
4743
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
4744
|
+
<div class="modal-header">
|
|
4745
|
+
<h3 id="help-title">Editing the Graph</h3>
|
|
4746
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4747
|
+
</div>
|
|
4748
|
+
<div class="modal-body">
|
|
4749
|
+
<p>Here's how to edit the graph:</p>
|
|
4750
|
+
<ul>
|
|
4751
|
+
<li><strong>Enable editing</strong>: Click "Edit"</li>
|
|
4752
|
+
<li><strong>Add a node</strong>: Double\u2011click an empty area</li>
|
|
4753
|
+
<li><strong>Edit a node</strong>: Double\u2011click a node</li>
|
|
4754
|
+
<li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
|
|
4755
|
+
<li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
|
|
4756
|
+
<li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
|
|
4757
|
+
<li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
|
|
4758
|
+
</ul>
|
|
4759
|
+
<p>When you're done, click "Done" to lock the canvas.</p>
|
|
4760
|
+
</div>
|
|
4761
|
+
</div>
|
|
4762
|
+
`;
|
|
4763
|
+
document.body.appendChild(this.helpOverlay);
|
|
4764
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
4765
|
+
const target = e.target;
|
|
4766
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4767
|
+
this.closeHelp();
|
|
4768
|
+
}
|
|
4769
|
+
});
|
|
4770
|
+
}
|
|
4771
|
+
this.helpOverlay.style.display = "flex";
|
|
4772
|
+
}
|
|
4773
|
+
closeHelp() {
|
|
4774
|
+
if (this.helpOverlay) this.helpOverlay.style.display = "none";
|
|
4775
|
+
}
|
|
4776
|
+
handleIngestMessage = async (msg) => {
|
|
4777
|
+
if (!this.ingest) return;
|
|
4778
|
+
await this.ingest.apply(msg);
|
|
4779
|
+
};
|
|
4780
|
+
updateSourceIcon() {
|
|
4781
|
+
const iconBtn = this.rootElement.querySelector("#source-icon");
|
|
4782
|
+
if (!iconBtn) return;
|
|
4783
|
+
iconBtn.classList.remove("active", "connecting", "error");
|
|
4784
|
+
const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
|
|
4785
|
+
const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
|
|
4786
|
+
const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
|
|
4787
|
+
let icon = "\u{1F4E1}";
|
|
4788
|
+
if (this.activeSourceType === "folder") {
|
|
4789
|
+
icon = "\u{1F4C1}";
|
|
4790
|
+
} else if (this.activeSourceType === "file") {
|
|
4791
|
+
icon = "\u{1F4C4}";
|
|
4792
|
+
}
|
|
4793
|
+
if (isConnected) {
|
|
4794
|
+
iconBtn.classList.add("active");
|
|
4795
|
+
iconBtn.textContent = icon;
|
|
4796
|
+
} else if (isConnecting) {
|
|
4797
|
+
iconBtn.classList.add("connecting");
|
|
4798
|
+
iconBtn.textContent = icon;
|
|
4799
|
+
} else if (hasError) {
|
|
4800
|
+
iconBtn.classList.add("error");
|
|
4801
|
+
iconBtn.textContent = icon;
|
|
4802
|
+
} else {
|
|
4803
|
+
iconBtn.textContent = "\u{1F4E1}";
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
updateWsStatus = (status, detail) => {
|
|
4807
|
+
if (status === "connecting" || status === "reconnecting") {
|
|
4808
|
+
this.wsStatus = "connecting";
|
|
4809
|
+
this.activeSourceType = "ws";
|
|
4810
|
+
} else if (status === "connected") {
|
|
4811
|
+
this.wsStatus = "connected";
|
|
4812
|
+
this.activeSourceType = "ws";
|
|
4813
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4814
|
+
this.fsSource.close();
|
|
4815
|
+
}
|
|
4816
|
+
if (this.fileSource && this.fileStatus === "connected") {
|
|
4817
|
+
this.fileSource.close();
|
|
4818
|
+
}
|
|
4819
|
+
} else if (status === "error") {
|
|
4820
|
+
this.wsStatus = "error";
|
|
4821
|
+
} else {
|
|
4822
|
+
this.wsStatus = "disconnected";
|
|
4823
|
+
if (this.activeSourceType === "ws") {
|
|
4824
|
+
this.activeSourceType = null;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
this.updateSourceIcon();
|
|
4828
|
+
this.updateSourceModal();
|
|
4829
|
+
};
|
|
4830
|
+
updateFsStatus = (status, detail) => {
|
|
4831
|
+
if (status === "opened") {
|
|
4832
|
+
this.fsStatus = "opening";
|
|
4833
|
+
this.activeSourceType = "folder";
|
|
4834
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4835
|
+
this.wsSource.disconnect();
|
|
4836
|
+
}
|
|
4837
|
+
} else if (status === "reading") {
|
|
4838
|
+
this.fsStatus = "connected";
|
|
4839
|
+
this.activeSourceType = "folder";
|
|
4840
|
+
} else if (status === "error") {
|
|
4841
|
+
this.fsStatus = "error";
|
|
4842
|
+
} else if (status === "closed") {
|
|
4843
|
+
this.fsStatus = "disconnected";
|
|
4844
|
+
if (this.activeSourceType === "folder") {
|
|
4845
|
+
this.activeSourceType = null;
|
|
4846
|
+
}
|
|
4847
|
+
} else {
|
|
4848
|
+
this.fsStatus = "disconnected";
|
|
4849
|
+
}
|
|
4850
|
+
this.updateSourceIcon();
|
|
4851
|
+
this.updateSourceModal();
|
|
4852
|
+
};
|
|
4853
|
+
updateFileStatus = (status, detail) => {
|
|
4854
|
+
if (status === "opened") {
|
|
4855
|
+
this.fileStatus = "connecting";
|
|
4856
|
+
this.activeSourceType = "file";
|
|
4857
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4858
|
+
this.wsSource.disconnect();
|
|
4859
|
+
}
|
|
4860
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4861
|
+
this.fsSource.close();
|
|
4862
|
+
}
|
|
4863
|
+
} else if (status === "reading") {
|
|
4864
|
+
this.fileStatus = "connected";
|
|
4865
|
+
this.activeSourceType = "file";
|
|
4866
|
+
} else if (status === "error") {
|
|
4867
|
+
this.fileStatus = "error";
|
|
4868
|
+
} else if (status === "closed") {
|
|
4869
|
+
this.fileStatus = "disconnected";
|
|
4870
|
+
if (this.activeSourceType === "file") {
|
|
4871
|
+
this.activeSourceType = null;
|
|
4872
|
+
}
|
|
4873
|
+
} else {
|
|
4874
|
+
this.fileStatus = "disconnected";
|
|
4875
|
+
}
|
|
4876
|
+
this.updateSourceIcon();
|
|
4877
|
+
this.updateSourceModal();
|
|
4878
|
+
};
|
|
4879
|
+
createSourceModal() {
|
|
4880
|
+
if (this.sourceModal) return this.sourceModal;
|
|
4881
|
+
this.sourceModal = document.createElement("div");
|
|
4882
|
+
this.sourceModal.className = "modal-overlay";
|
|
4883
|
+
this.sourceModal.style.display = "none";
|
|
4884
|
+
this.sourceModal.innerHTML = `
|
|
4885
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
|
|
4886
|
+
<div class="modal-header">
|
|
4887
|
+
<h3 id="source-modal-title">Data Source Connection</h3>
|
|
4888
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4889
|
+
</div>
|
|
4890
|
+
<div class="modal-body">
|
|
4891
|
+
<div class="source-type-selector">
|
|
4892
|
+
<button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
|
|
4893
|
+
<button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
|
|
4894
|
+
</div>
|
|
4895
|
+
|
|
4896
|
+
<div class="source-controls" data-source="ws">
|
|
4897
|
+
<div class="form-group">
|
|
4898
|
+
<label for="source-modal-url">WebSocket URL</label>
|
|
4899
|
+
<input type="text" id="source-modal-url" value="${this.wsUrl}" />
|
|
4900
|
+
</div>
|
|
4901
|
+
<div class="button-group">
|
|
4902
|
+
<button id="source-modal-connect-ws" class="primary">Connect</button>
|
|
4903
|
+
<button id="source-modal-disconnect-ws">Disconnect</button>
|
|
4904
|
+
<button id="source-modal-change-ws">Change Connection</button>
|
|
4905
|
+
</div>
|
|
4906
|
+
</div>
|
|
4907
|
+
|
|
4908
|
+
<div class="source-controls" data-source="folder">
|
|
4909
|
+
<div class="form-group">
|
|
4910
|
+
<label>File System Source</label>
|
|
4911
|
+
<p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
|
|
4912
|
+
Select a directory containing a graph.ndjson file to watch for changes.
|
|
4913
|
+
</p>
|
|
4914
|
+
</div>
|
|
4915
|
+
<div class="button-group">
|
|
4916
|
+
<button id="source-modal-connect-folder" class="primary">Open Folder</button>
|
|
4917
|
+
<button id="source-modal-disconnect-folder">Disconnect</button>
|
|
4918
|
+
</div>
|
|
4919
|
+
</div>
|
|
4920
|
+
|
|
4921
|
+
<div id="source-modal-status"></div>
|
|
4922
|
+
</div>
|
|
4923
|
+
</div>
|
|
4924
|
+
`;
|
|
4925
|
+
document.body.appendChild(this.sourceModal);
|
|
4926
|
+
this.sourceModal.addEventListener("click", (e) => {
|
|
4927
|
+
const target = e.target;
|
|
4928
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4929
|
+
this.closeSourceModal();
|
|
4930
|
+
}
|
|
4931
|
+
});
|
|
4932
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4933
|
+
btn.addEventListener("click", () => {
|
|
4934
|
+
const sourceType = btn.getAttribute("data-source");
|
|
4935
|
+
if (sourceType) {
|
|
4936
|
+
this.selectSourceType(sourceType);
|
|
4937
|
+
}
|
|
4938
|
+
});
|
|
4939
|
+
});
|
|
4940
|
+
document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
|
|
4941
|
+
document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
|
|
4942
|
+
document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
|
|
4943
|
+
document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
|
|
4944
|
+
document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
|
|
4945
|
+
document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
|
|
4946
|
+
if (e.key === "Enter" && this.wsStatus !== "connected") {
|
|
4947
|
+
this.handleConnect();
|
|
4948
|
+
}
|
|
4949
|
+
});
|
|
4950
|
+
return this.sourceModal;
|
|
4951
|
+
}
|
|
4952
|
+
selectSourceType(type, skipUpdate = false) {
|
|
4953
|
+
if (!this.sourceModal) return;
|
|
4954
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4955
|
+
if (btn.getAttribute("data-source") === type) {
|
|
4956
|
+
btn.classList.add("active");
|
|
4957
|
+
} else {
|
|
4958
|
+
btn.classList.remove("active");
|
|
4959
|
+
}
|
|
4960
|
+
});
|
|
4961
|
+
this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
|
|
4962
|
+
if (controls.getAttribute("data-source") === type) {
|
|
4963
|
+
controls.classList.add("active");
|
|
4964
|
+
} else {
|
|
4965
|
+
controls.classList.remove("active");
|
|
4966
|
+
}
|
|
4967
|
+
});
|
|
4968
|
+
if (!skipUpdate) {
|
|
4969
|
+
this.updateSourceModalContent();
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
updateSourceModalContent() {
|
|
4973
|
+
if (!this.sourceModal) return;
|
|
4974
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
4975
|
+
const connectWsBtn = document.getElementById("source-modal-connect-ws");
|
|
4976
|
+
const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
|
|
4977
|
+
const changeWsBtn = document.getElementById("source-modal-change-ws");
|
|
4978
|
+
if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
|
|
4979
|
+
const isWsConnected = this.wsStatus === "connected";
|
|
4980
|
+
const isWsConnecting = this.wsStatus === "connecting";
|
|
4981
|
+
connectWsBtn.disabled = isWsConnected || isWsConnecting;
|
|
4982
|
+
disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4983
|
+
changeWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4984
|
+
urlInput.disabled = isWsConnecting;
|
|
4985
|
+
}
|
|
4986
|
+
const connectFolderBtn = document.getElementById("source-modal-connect-folder");
|
|
4987
|
+
const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
|
|
4988
|
+
if (connectFolderBtn && disconnectFolderBtn) {
|
|
4989
|
+
const isFolderConnected = this.fsStatus === "connected";
|
|
4990
|
+
const isFolderOpening = this.fsStatus === "opening";
|
|
4991
|
+
connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
|
|
4992
|
+
disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
|
|
4993
|
+
}
|
|
4994
|
+
const statusDiv = document.getElementById("source-modal-status");
|
|
4995
|
+
if (!statusDiv) return;
|
|
4996
|
+
const currentUrl = urlInput?.value || this.wsUrl;
|
|
4997
|
+
statusDiv.innerHTML = "";
|
|
4998
|
+
if (this.activeSourceType === "ws") {
|
|
4999
|
+
if (this.wsStatus === "connecting") {
|
|
5000
|
+
statusDiv.innerHTML = `
|
|
5001
|
+
<div class="status-message info">
|
|
5002
|
+
<span class="loading-spinner"></span>
|
|
5003
|
+
Connecting to ${currentUrl}...
|
|
5004
|
+
</div>
|
|
5005
|
+
`;
|
|
5006
|
+
} else if (this.wsStatus === "connected") {
|
|
5007
|
+
statusDiv.innerHTML = `
|
|
5008
|
+
<div class="status-message success">
|
|
5009
|
+
\u2713 Connected to ${currentUrl}
|
|
5010
|
+
</div>
|
|
5011
|
+
`;
|
|
5012
|
+
} else if (this.wsStatus === "error") {
|
|
5013
|
+
statusDiv.innerHTML = `
|
|
5014
|
+
<div class="status-message error">
|
|
5015
|
+
\u2717 Connection error. Please check the URL and try again.
|
|
5016
|
+
</div>
|
|
5017
|
+
`;
|
|
5018
|
+
} else {
|
|
5019
|
+
statusDiv.innerHTML = `
|
|
5020
|
+
<div class="status-message info">
|
|
5021
|
+
Not connected
|
|
5022
|
+
</div>
|
|
5023
|
+
`;
|
|
5024
|
+
}
|
|
5025
|
+
} else if (this.activeSourceType === "folder") {
|
|
5026
|
+
if (this.fsStatus === "opening") {
|
|
5027
|
+
statusDiv.innerHTML = `
|
|
5028
|
+
<div class="status-message info">
|
|
5029
|
+
<span class="loading-spinner"></span>
|
|
5030
|
+
Opening folder...
|
|
5031
|
+
</div>
|
|
5032
|
+
`;
|
|
5033
|
+
} else if (this.fsStatus === "connected") {
|
|
5034
|
+
statusDiv.innerHTML = `
|
|
5035
|
+
<div class="status-message success">
|
|
5036
|
+
\u2713 Folder connected and watching for changes
|
|
5037
|
+
</div>
|
|
5038
|
+
`;
|
|
5039
|
+
} else if (this.fsStatus === "error") {
|
|
5040
|
+
statusDiv.innerHTML = `
|
|
5041
|
+
<div class="status-message error">
|
|
5042
|
+
\u2717 Error opening folder. Please try again.
|
|
5043
|
+
</div>
|
|
5044
|
+
`;
|
|
5045
|
+
} else {
|
|
5046
|
+
statusDiv.innerHTML = `
|
|
5047
|
+
<div class="status-message info">
|
|
5048
|
+
Not connected
|
|
5049
|
+
</div>
|
|
5050
|
+
`;
|
|
5051
|
+
}
|
|
5052
|
+
} else if (this.activeSourceType === "file") {
|
|
5053
|
+
const example = this.examples[this.currentExample];
|
|
5054
|
+
const filePath = example.source?.type === "file" ? example.source.path : "";
|
|
5055
|
+
if (this.fileStatus === "connecting") {
|
|
5056
|
+
statusDiv.innerHTML = `
|
|
5057
|
+
<div class="status-message info">
|
|
5058
|
+
<span class="loading-spinner"></span>
|
|
5059
|
+
Connecting to ${filePath}...
|
|
5060
|
+
</div>
|
|
5061
|
+
`;
|
|
5062
|
+
} else if (this.fileStatus === "connected") {
|
|
5063
|
+
statusDiv.innerHTML = `
|
|
5064
|
+
<div class="status-message success">
|
|
5065
|
+
\u2713 Connected to ${filePath}
|
|
5066
|
+
</div>
|
|
5067
|
+
`;
|
|
5068
|
+
} else if (this.fileStatus === "error") {
|
|
5069
|
+
statusDiv.innerHTML = `
|
|
5070
|
+
<div class="status-message error">
|
|
5071
|
+
\u2717 Error loading file. Please check the path and try again.
|
|
5072
|
+
</div>
|
|
5073
|
+
`;
|
|
5074
|
+
} else {
|
|
5075
|
+
statusDiv.innerHTML = `
|
|
5076
|
+
<div class="status-message info">
|
|
5077
|
+
Not connected
|
|
5078
|
+
</div>
|
|
5079
|
+
`;
|
|
5080
|
+
}
|
|
5081
|
+
} else {
|
|
5082
|
+
statusDiv.innerHTML = `
|
|
5083
|
+
<div class="status-message info">
|
|
5084
|
+
Select a source type to connect
|
|
5085
|
+
</div>
|
|
5086
|
+
`;
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
updateSourceModal() {
|
|
5090
|
+
if (!this.sourceModal) return;
|
|
5091
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5092
|
+
this.selectSourceType(activeType, true);
|
|
5093
|
+
this.updateSourceModalContent();
|
|
5094
|
+
}
|
|
5095
|
+
openSourceModal() {
|
|
5096
|
+
this.createSourceModal();
|
|
5097
|
+
if (this.sourceModal) {
|
|
5098
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5099
|
+
if (urlInput) {
|
|
5100
|
+
urlInput.value = this.wsUrl;
|
|
5101
|
+
}
|
|
5102
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5103
|
+
this.selectSourceType(activeType);
|
|
5104
|
+
this.updateSourceModal();
|
|
5105
|
+
this.sourceModal.style.display = "flex";
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
closeSourceModal() {
|
|
5109
|
+
if (this.sourceModal) {
|
|
5110
|
+
this.sourceModal.style.display = "none";
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
handleConnect() {
|
|
5114
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5115
|
+
if (!urlInput) return;
|
|
5116
|
+
const url = urlInput.value.trim() || "ws://localhost:8787";
|
|
5117
|
+
this.wsUrl = url;
|
|
5118
|
+
if (this.wsSource) {
|
|
5119
|
+
this.wsSource.disconnect();
|
|
5120
|
+
}
|
|
5121
|
+
this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
5122
|
+
this.wsSource.connect();
|
|
5123
|
+
this.updateSourceModal();
|
|
5124
|
+
}
|
|
5125
|
+
handleDisconnect() {
|
|
5126
|
+
this.wsSource?.disconnect();
|
|
5127
|
+
this.updateSourceModal();
|
|
5128
|
+
}
|
|
5129
|
+
handleChangeConnection() {
|
|
5130
|
+
if (this.wsSource) {
|
|
5131
|
+
this.wsSource.disconnect();
|
|
5132
|
+
}
|
|
5133
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5134
|
+
if (urlInput) {
|
|
5135
|
+
urlInput.focus();
|
|
5136
|
+
urlInput.select();
|
|
5137
|
+
}
|
|
5138
|
+
this.updateSourceModal();
|
|
5139
|
+
}
|
|
5140
|
+
async handleOpenFolder() {
|
|
5141
|
+
if (!this.fsSource) {
|
|
5142
|
+
this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
|
|
5143
|
+
}
|
|
5144
|
+
this.updateSourceModal();
|
|
5145
|
+
await this.fsSource.openDirectory();
|
|
5146
|
+
}
|
|
5147
|
+
handleCloseFolder() {
|
|
5148
|
+
this.fsSource?.close();
|
|
5149
|
+
this.updateSourceModal();
|
|
5150
|
+
}
|
|
5151
|
+
/**
|
|
5152
|
+
* Add or update an example
|
|
5153
|
+
*/
|
|
5154
|
+
addExample(key, example) {
|
|
5155
|
+
this.examples[key] = example;
|
|
5156
|
+
this.updateExampleList();
|
|
5157
|
+
if (this.currentExample === key) {
|
|
5158
|
+
this.renderGraph();
|
|
5159
|
+
this.connectExampleSource();
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
5162
|
+
/**
|
|
5163
|
+
* Remove an example
|
|
5164
|
+
*/
|
|
5165
|
+
removeExample(key) {
|
|
5166
|
+
delete this.examples[key];
|
|
5167
|
+
if (this.currentExample === key) {
|
|
5168
|
+
this.exampleList = Object.keys(this.examples);
|
|
5169
|
+
this.currentExample = this.exampleList[0] || "";
|
|
5170
|
+
if (this.currentExample) {
|
|
5171
|
+
this.renderGraph();
|
|
5172
|
+
this.connectExampleSource();
|
|
5173
|
+
}
|
|
5174
|
+
}
|
|
5175
|
+
this.updateExampleList();
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* Update the example list in the DOM
|
|
5179
|
+
*/
|
|
5180
|
+
updateExampleList() {
|
|
5181
|
+
this.exampleList = Object.keys(this.examples);
|
|
5182
|
+
const exampleListEl = this.rootElement.querySelector(".example-list");
|
|
5183
|
+
if (!exampleListEl) return;
|
|
5184
|
+
exampleListEl.innerHTML = this.exampleList.map((key, i) => {
|
|
5185
|
+
const example = this.examples[key];
|
|
5186
|
+
const isActive = key === this.currentExample;
|
|
5187
|
+
return `
|
|
5188
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
5189
|
+
${example.name}
|
|
5190
|
+
</button>
|
|
5191
|
+
`;
|
|
5192
|
+
}).join("");
|
|
5193
|
+
exampleListEl.querySelectorAll(".example-btn").forEach((btn) => {
|
|
5194
|
+
btn.addEventListener("click", () => {
|
|
5195
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
5196
|
+
btn.classList.add("active");
|
|
5197
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
5198
|
+
this.renderGraph();
|
|
5199
|
+
this.connectExampleSource();
|
|
5200
|
+
});
|
|
5201
|
+
});
|
|
5202
|
+
}
|
|
4041
5203
|
};
|
|
4042
5204
|
|
|
4043
5205
|
// src/index.ts
|
|
@@ -4048,6 +5210,12 @@ async function graph(args = { root: "app" }) {
|
|
|
4048
5210
|
}
|
|
4049
5211
|
var index_default = graph;
|
|
4050
5212
|
export {
|
|
5213
|
+
FileSource,
|
|
5214
|
+
FileSystemSource,
|
|
5215
|
+
Ingest,
|
|
5216
|
+
Playground,
|
|
5217
|
+
Updater,
|
|
5218
|
+
WebSocketSource,
|
|
4051
5219
|
index_default as default,
|
|
4052
5220
|
graph
|
|
4053
5221
|
};
|