@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.cjs
CHANGED
|
@@ -30,6 +30,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
FileSource: () => FileSource,
|
|
34
|
+
FileSystemSource: () => FileSystemSource,
|
|
35
|
+
Ingest: () => Ingest,
|
|
36
|
+
Playground: () => Playground,
|
|
37
|
+
Updater: () => Updater,
|
|
38
|
+
WebSocketSource: () => WebSocketSource,
|
|
33
39
|
default: () => index_default,
|
|
34
40
|
graph: () => graph
|
|
35
41
|
});
|
|
@@ -1375,15 +1381,16 @@ var Layout = class _Layout {
|
|
|
1375
1381
|
|
|
1376
1382
|
// src/canvas/marker.tsx
|
|
1377
1383
|
var import_jsx_runtime = require("jsx-dom/jsx-runtime");
|
|
1378
|
-
function arrow(size, reverse = false) {
|
|
1384
|
+
function arrow(size, reverse = false, prefix = "") {
|
|
1379
1385
|
const h = size / 1.5;
|
|
1380
1386
|
const w = size;
|
|
1381
1387
|
const ry = h / 2;
|
|
1382
1388
|
const suffix = reverse ? "-reverse" : "";
|
|
1389
|
+
const id = prefix ? `${prefix}-g3p-marker-arrow${suffix}` : `g3p-marker-arrow${suffix}`;
|
|
1383
1390
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1384
1391
|
"marker",
|
|
1385
1392
|
{
|
|
1386
|
-
id
|
|
1393
|
+
id,
|
|
1387
1394
|
className: "g3p-marker g3p-marker-arrow",
|
|
1388
1395
|
markerWidth: size,
|
|
1389
1396
|
markerHeight: size,
|
|
@@ -1395,14 +1402,15 @@ function arrow(size, reverse = false) {
|
|
|
1395
1402
|
}
|
|
1396
1403
|
);
|
|
1397
1404
|
}
|
|
1398
|
-
function circle(size, reverse = false) {
|
|
1405
|
+
function circle(size, reverse = false, prefix = "") {
|
|
1399
1406
|
const r = size / 3;
|
|
1400
1407
|
const cy = size / 2;
|
|
1401
1408
|
const suffix = reverse ? "-reverse" : "";
|
|
1409
|
+
const id = prefix ? `${prefix}-g3p-marker-circle${suffix}` : `g3p-marker-circle${suffix}`;
|
|
1402
1410
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1403
1411
|
"marker",
|
|
1404
1412
|
{
|
|
1405
|
-
id
|
|
1413
|
+
id,
|
|
1406
1414
|
className: "g3p-marker g3p-marker-circle",
|
|
1407
1415
|
markerWidth: size,
|
|
1408
1416
|
markerHeight: size,
|
|
@@ -1414,15 +1422,16 @@ function circle(size, reverse = false) {
|
|
|
1414
1422
|
}
|
|
1415
1423
|
);
|
|
1416
1424
|
}
|
|
1417
|
-
function diamond(size, reverse = false) {
|
|
1425
|
+
function diamond(size, reverse = false, prefix = "") {
|
|
1418
1426
|
const w = size * 0.7;
|
|
1419
1427
|
const h = size / 2;
|
|
1420
1428
|
const cy = size / 2;
|
|
1421
1429
|
const suffix = reverse ? "-reverse" : "";
|
|
1430
|
+
const id = prefix ? `${prefix}-g3p-marker-diamond${suffix}` : `g3p-marker-diamond${suffix}`;
|
|
1422
1431
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1423
1432
|
"marker",
|
|
1424
1433
|
{
|
|
1425
|
-
id
|
|
1434
|
+
id,
|
|
1426
1435
|
className: "g3p-marker g3p-marker-diamond",
|
|
1427
1436
|
markerWidth: size,
|
|
1428
1437
|
markerHeight: size,
|
|
@@ -1434,14 +1443,15 @@ function diamond(size, reverse = false) {
|
|
|
1434
1443
|
}
|
|
1435
1444
|
);
|
|
1436
1445
|
}
|
|
1437
|
-
function bar(size, reverse = false) {
|
|
1446
|
+
function bar(size, reverse = false, prefix = "") {
|
|
1438
1447
|
const h = size * 0.6;
|
|
1439
1448
|
const cy = size / 2;
|
|
1440
1449
|
const suffix = reverse ? "-reverse" : "";
|
|
1450
|
+
const id = prefix ? `${prefix}-g3p-marker-bar${suffix}` : `g3p-marker-bar${suffix}`;
|
|
1441
1451
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1442
1452
|
"marker",
|
|
1443
1453
|
{
|
|
1444
|
-
id
|
|
1454
|
+
id,
|
|
1445
1455
|
className: "g3p-marker g3p-marker-bar",
|
|
1446
1456
|
markerWidth: size,
|
|
1447
1457
|
markerHeight: size,
|
|
@@ -1453,7 +1463,7 @@ function bar(size, reverse = false) {
|
|
|
1453
1463
|
}
|
|
1454
1464
|
);
|
|
1455
1465
|
}
|
|
1456
|
-
function none(size, reverse = false) {
|
|
1466
|
+
function none(size, reverse = false, prefix = "") {
|
|
1457
1467
|
return void 0;
|
|
1458
1468
|
}
|
|
1459
1469
|
function normalize(data) {
|
|
@@ -2235,6 +2245,9 @@ var Seg2 = class {
|
|
|
2235
2245
|
if (this.source.isDummy) source = void 0;
|
|
2236
2246
|
if (this.target.isDummy) target = void 0;
|
|
2237
2247
|
const typeClass = this.type ? `g3p-edge-type-${this.type}` : "";
|
|
2248
|
+
const prefix = this.canvas.markerPrefix;
|
|
2249
|
+
const markerStartId = source ? prefix ? `${prefix}-g3p-marker-${source}-reverse` : `g3p-marker-${source}-reverse` : void 0;
|
|
2250
|
+
const markerEndId = target ? prefix ? `${prefix}-g3p-marker-${target}` : `g3p-marker-${target}` : void 0;
|
|
2238
2251
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
2239
2252
|
"g",
|
|
2240
2253
|
{
|
|
@@ -2249,8 +2262,8 @@ var Seg2 = class {
|
|
|
2249
2262
|
d: this.svg,
|
|
2250
2263
|
fill: "none",
|
|
2251
2264
|
className: "g3p-seg-line",
|
|
2252
|
-
markerStart:
|
|
2253
|
-
markerEnd:
|
|
2265
|
+
markerStart: markerStartId ? `url(#${markerStartId})` : void 0,
|
|
2266
|
+
markerEnd: markerEndId ? `url(#${markerEndId})` : void 0
|
|
2254
2267
|
}
|
|
2255
2268
|
),
|
|
2256
2269
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -2835,6 +2848,10 @@ var Canvas = class {
|
|
|
2835
2848
|
curNodes;
|
|
2836
2849
|
curSegs;
|
|
2837
2850
|
updating;
|
|
2851
|
+
// Unique marker ID prefix for this canvas instance
|
|
2852
|
+
markerPrefix;
|
|
2853
|
+
// Dynamic style element for this instance (for cleanup)
|
|
2854
|
+
dynamicStyleEl;
|
|
2838
2855
|
// Pan-zoom state
|
|
2839
2856
|
panScale = null;
|
|
2840
2857
|
zoomControls;
|
|
@@ -2848,6 +2865,13 @@ var Canvas = class {
|
|
|
2848
2865
|
constructor(api, options) {
|
|
2849
2866
|
Object.assign(this, options);
|
|
2850
2867
|
this.api = api;
|
|
2868
|
+
this.reset();
|
|
2869
|
+
this.markerPrefix = api.root.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
2870
|
+
this.createMeasurementContainer();
|
|
2871
|
+
this.createCanvasContainer();
|
|
2872
|
+
if (this.panZoom) this.setupPanZoom();
|
|
2873
|
+
}
|
|
2874
|
+
reset() {
|
|
2851
2875
|
this.allNodes = /* @__PURE__ */ new Map();
|
|
2852
2876
|
this.curNodes = /* @__PURE__ */ new Map();
|
|
2853
2877
|
this.curSegs = /* @__PURE__ */ new Map();
|
|
@@ -2856,9 +2880,7 @@ var Canvas = class {
|
|
|
2856
2880
|
this.transform = { x: 0, y: 0, scale: 1 };
|
|
2857
2881
|
this.editMode = new EditMode();
|
|
2858
2882
|
this.editMode.editable = this.editable;
|
|
2859
|
-
this.
|
|
2860
|
-
this.createCanvasContainer();
|
|
2861
|
-
if (this.panZoom) this.setupPanZoom();
|
|
2883
|
+
if (this.group) this.group.innerHTML = "";
|
|
2862
2884
|
}
|
|
2863
2885
|
createMeasurementContainer() {
|
|
2864
2886
|
this.measurement = document.createElement("div");
|
|
@@ -3043,12 +3065,13 @@ var Canvas = class {
|
|
|
3043
3065
|
}
|
|
3044
3066
|
generateDynamicStyles() {
|
|
3045
3067
|
let css = "";
|
|
3046
|
-
|
|
3068
|
+
const scope = `[data-g3p-instance="${this.markerPrefix}"]`;
|
|
3069
|
+
css += themeToCSS(this.theme, scope);
|
|
3047
3070
|
for (const [type, vars] of Object.entries(this.nodeTypes)) {
|
|
3048
|
-
css += themeToCSS(vars,
|
|
3071
|
+
css += themeToCSS(vars, `${scope} .g3p-node-type-${type}`, "node");
|
|
3049
3072
|
}
|
|
3050
3073
|
for (const [type, vars] of Object.entries(this.edgeTypes)) {
|
|
3051
|
-
css += themeToCSS(vars,
|
|
3074
|
+
css += themeToCSS(vars, `${scope} .g3p-edge-type-${type}`);
|
|
3052
3075
|
}
|
|
3053
3076
|
return css;
|
|
3054
3077
|
}
|
|
@@ -3061,15 +3084,18 @@ var Canvas = class {
|
|
|
3061
3084
|
}
|
|
3062
3085
|
const dynamicStyles = this.generateDynamicStyles();
|
|
3063
3086
|
if (dynamicStyles) {
|
|
3064
|
-
|
|
3065
|
-
dynamicStyleEl
|
|
3066
|
-
|
|
3087
|
+
this.dynamicStyleEl?.remove();
|
|
3088
|
+
this.dynamicStyleEl = document.createElement("style");
|
|
3089
|
+
this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
|
|
3090
|
+
this.dynamicStyleEl.textContent = dynamicStyles;
|
|
3091
|
+
document.head.appendChild(this.dynamicStyleEl);
|
|
3067
3092
|
}
|
|
3068
3093
|
const colorModeClass = this.colorMode !== "system" ? `g3p-${this.colorMode}` : "";
|
|
3069
3094
|
this.container = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
3070
3095
|
"div",
|
|
3071
3096
|
{
|
|
3072
3097
|
className: `g3p-canvas-container ${colorModeClass}`.trim(),
|
|
3098
|
+
"data-g3p-instance": this.markerPrefix,
|
|
3073
3099
|
ref: (el) => this.container = el,
|
|
3074
3100
|
onContextMenu: this.onContextMenu.bind(this),
|
|
3075
3101
|
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
@@ -3085,8 +3111,8 @@ var Canvas = class {
|
|
|
3085
3111
|
onDblClick: this.onDoubleClick.bind(this),
|
|
3086
3112
|
children: [
|
|
3087
3113
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("defs", { children: [
|
|
3088
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize, false)),
|
|
3089
|
-
Object.values(markerDefs).map((marker) => marker(this.markerSize, true))
|
|
3114
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, false, this.markerPrefix)),
|
|
3115
|
+
Object.values(markerDefs).map((marker) => marker(this.markerSize, true, this.markerPrefix))
|
|
3090
3116
|
] }),
|
|
3091
3117
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
3092
3118
|
"g",
|
|
@@ -3443,6 +3469,40 @@ var Canvas = class {
|
|
|
3443
3469
|
}
|
|
3444
3470
|
return { type: "canvas" };
|
|
3445
3471
|
}
|
|
3472
|
+
/** Update theme and type styles dynamically */
|
|
3473
|
+
updateStyles(options) {
|
|
3474
|
+
if (options.theme !== void 0) this.theme = options.theme;
|
|
3475
|
+
if (options.nodeTypes !== void 0) this.nodeTypes = options.nodeTypes;
|
|
3476
|
+
if (options.edgeTypes !== void 0) this.edgeTypes = options.edgeTypes;
|
|
3477
|
+
const dynamicStyles = this.generateDynamicStyles();
|
|
3478
|
+
if (dynamicStyles) {
|
|
3479
|
+
if (!this.dynamicStyleEl) {
|
|
3480
|
+
this.dynamicStyleEl = document.createElement("style");
|
|
3481
|
+
this.dynamicStyleEl.id = `g3p-styles-${this.markerPrefix}`;
|
|
3482
|
+
document.head.appendChild(this.dynamicStyleEl);
|
|
3483
|
+
}
|
|
3484
|
+
this.dynamicStyleEl.textContent = dynamicStyles;
|
|
3485
|
+
} else if (this.dynamicStyleEl) {
|
|
3486
|
+
this.dynamicStyleEl.remove();
|
|
3487
|
+
this.dynamicStyleEl = void 0;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
/** Update color mode without recreating the canvas */
|
|
3491
|
+
setColorMode(colorMode) {
|
|
3492
|
+
if (!this.container) return;
|
|
3493
|
+
this.colorMode = colorMode;
|
|
3494
|
+
this.container.classList.remove("g3p-light", "g3p-dark");
|
|
3495
|
+
if (colorMode !== "system") {
|
|
3496
|
+
this.container.classList.add(`g3p-${colorMode}`);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
/** Cleanup resources when the canvas is destroyed */
|
|
3500
|
+
destroy() {
|
|
3501
|
+
this.dynamicStyleEl?.remove();
|
|
3502
|
+
this.dynamicStyleEl = void 0;
|
|
3503
|
+
this.measurement?.remove();
|
|
3504
|
+
this.measurement = void 0;
|
|
3505
|
+
}
|
|
3446
3506
|
};
|
|
3447
3507
|
var themeVarMap = {
|
|
3448
3508
|
// Canvas
|
|
@@ -3557,26 +3617,50 @@ var Updater = class _Updater {
|
|
|
3557
3617
|
this.update.addNodes.push(node);
|
|
3558
3618
|
return this;
|
|
3559
3619
|
}
|
|
3620
|
+
addNodes(...nodes) {
|
|
3621
|
+
this.update.addNodes.push(...nodes);
|
|
3622
|
+
return this;
|
|
3623
|
+
}
|
|
3560
3624
|
deleteNode(node) {
|
|
3561
3625
|
this.update.removeNodes.push(node);
|
|
3562
3626
|
return this;
|
|
3563
3627
|
}
|
|
3628
|
+
deleteNodes(...nodes) {
|
|
3629
|
+
this.update.removeNodes.push(...nodes);
|
|
3630
|
+
return this;
|
|
3631
|
+
}
|
|
3564
3632
|
updateNode(node) {
|
|
3565
3633
|
this.update.updateNodes.push(node);
|
|
3566
3634
|
return this;
|
|
3567
3635
|
}
|
|
3636
|
+
updateNodes(...nodes) {
|
|
3637
|
+
this.update.updateNodes.push(...nodes);
|
|
3638
|
+
return this;
|
|
3639
|
+
}
|
|
3568
3640
|
addEdge(edge) {
|
|
3569
3641
|
this.update.addEdges.push(edge);
|
|
3570
3642
|
return this;
|
|
3571
3643
|
}
|
|
3644
|
+
addEdges(...edges) {
|
|
3645
|
+
this.update.addEdges.push(...edges);
|
|
3646
|
+
return this;
|
|
3647
|
+
}
|
|
3572
3648
|
deleteEdge(edge) {
|
|
3573
3649
|
this.update.removeEdges.push(edge);
|
|
3574
3650
|
return this;
|
|
3575
3651
|
}
|
|
3652
|
+
deleteEdges(...edges) {
|
|
3653
|
+
this.update.removeEdges.push(...edges);
|
|
3654
|
+
return this;
|
|
3655
|
+
}
|
|
3576
3656
|
updateEdge(edge) {
|
|
3577
3657
|
this.update.updateEdges.push(edge);
|
|
3578
3658
|
return this;
|
|
3579
3659
|
}
|
|
3660
|
+
updateEdges(...edges) {
|
|
3661
|
+
this.update.updateEdges.push(...edges);
|
|
3662
|
+
return this;
|
|
3663
|
+
}
|
|
3580
3664
|
static add(nodes, edges) {
|
|
3581
3665
|
const updater = new _Updater();
|
|
3582
3666
|
updater.update.addNodes = nodes;
|
|
@@ -3607,19 +3691,8 @@ var API = class {
|
|
|
3607
3691
|
constructor(args) {
|
|
3608
3692
|
this.root = args.root;
|
|
3609
3693
|
this.options = applyDefaults(args.options);
|
|
3610
|
-
let graph2 = new Graph({ options: this.options.graph });
|
|
3611
|
-
this.state = { graph: graph2, update: null };
|
|
3612
3694
|
this.events = args.events || {};
|
|
3613
|
-
this.
|
|
3614
|
-
this.index = 0;
|
|
3615
|
-
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3616
|
-
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3617
|
-
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3618
|
-
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3619
|
-
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3620
|
-
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3621
|
-
this.nextNodeId = 1;
|
|
3622
|
-
this.nextEdgeId = 1;
|
|
3695
|
+
this.reset();
|
|
3623
3696
|
this.canvas = new Canvas(this, {
|
|
3624
3697
|
...this.options.canvas,
|
|
3625
3698
|
dummyNodeSize: this.options.graph.dummyNodeSize,
|
|
@@ -3633,6 +3706,35 @@ var API = class {
|
|
|
3633
3706
|
this.history = [];
|
|
3634
3707
|
}
|
|
3635
3708
|
}
|
|
3709
|
+
reset() {
|
|
3710
|
+
let graph2 = new Graph({ options: this.options.graph });
|
|
3711
|
+
this.state = { graph: graph2, update: null };
|
|
3712
|
+
this.seq = [this.state];
|
|
3713
|
+
this.index = 0;
|
|
3714
|
+
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3715
|
+
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3716
|
+
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3717
|
+
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3718
|
+
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3719
|
+
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3720
|
+
this.nextNodeId = 1;
|
|
3721
|
+
this.nextEdgeId = 1;
|
|
3722
|
+
this.canvas?.reset?.();
|
|
3723
|
+
}
|
|
3724
|
+
/** Initialize the API */
|
|
3725
|
+
async init() {
|
|
3726
|
+
const root = document.getElementById(this.root);
|
|
3727
|
+
if (!root) throw new Error("root element not found");
|
|
3728
|
+
root.appendChild(this.canvas.container);
|
|
3729
|
+
await this.applyHistory();
|
|
3730
|
+
if (this.events.onInit) {
|
|
3731
|
+
this.events.onInit();
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
async applyHistory() {
|
|
3735
|
+
for (const update of this.history)
|
|
3736
|
+
await this.applyUpdate(update);
|
|
3737
|
+
}
|
|
3636
3738
|
/** Current history index (0-based) */
|
|
3637
3739
|
getHistoryIndex() {
|
|
3638
3740
|
return this.index;
|
|
@@ -3645,17 +3747,25 @@ var API = class {
|
|
|
3645
3747
|
setEditable(editable) {
|
|
3646
3748
|
this.canvas.editMode.editable = editable;
|
|
3647
3749
|
}
|
|
3750
|
+
/** Replace entire history (clears prior) */
|
|
3751
|
+
async replaceHistory(frames) {
|
|
3752
|
+
this.reset();
|
|
3753
|
+
this.history = frames;
|
|
3754
|
+
await this.applyHistory();
|
|
3755
|
+
}
|
|
3756
|
+
/** Rebuild from snapshot (nodes/edges) */
|
|
3757
|
+
async replaceSnapshot(nodes, edges, description) {
|
|
3758
|
+
this.reset();
|
|
3759
|
+
this.history = [{
|
|
3760
|
+
addNodes: nodes,
|
|
3761
|
+
addEdges: edges,
|
|
3762
|
+
description
|
|
3763
|
+
}];
|
|
3764
|
+
await this.applyHistory();
|
|
3765
|
+
}
|
|
3648
3766
|
get graph() {
|
|
3649
3767
|
return this.state.graph;
|
|
3650
3768
|
}
|
|
3651
|
-
/** Initialize the API */
|
|
3652
|
-
async init() {
|
|
3653
|
-
const root = document.getElementById(this.root);
|
|
3654
|
-
if (!root) throw new Error("root element not found");
|
|
3655
|
-
root.appendChild(this.canvas.container);
|
|
3656
|
-
for (const update of this.history)
|
|
3657
|
-
await this.applyUpdate(update);
|
|
3658
|
-
}
|
|
3659
3769
|
/** Navigate to a different state */
|
|
3660
3770
|
nav(nav) {
|
|
3661
3771
|
let newIndex;
|
|
@@ -4075,6 +4185,1064 @@ var API = class {
|
|
|
4075
4185
|
else
|
|
4076
4186
|
await this.deleteEdge(edge.data);
|
|
4077
4187
|
}
|
|
4188
|
+
/** Update theme and type styles dynamically */
|
|
4189
|
+
updateStyles(options) {
|
|
4190
|
+
this.canvas?.updateStyles(options);
|
|
4191
|
+
}
|
|
4192
|
+
/** Update color mode without recreating the canvas */
|
|
4193
|
+
setColorMode(colorMode) {
|
|
4194
|
+
this.canvas?.setColorMode(colorMode);
|
|
4195
|
+
}
|
|
4196
|
+
/** Cleanup resources when the graph is destroyed */
|
|
4197
|
+
destroy() {
|
|
4198
|
+
this.canvas?.destroy();
|
|
4199
|
+
}
|
|
4200
|
+
};
|
|
4201
|
+
|
|
4202
|
+
// src/api/ingest.ts
|
|
4203
|
+
var Ingest = class {
|
|
4204
|
+
constructor(api) {
|
|
4205
|
+
this.api = api;
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* Apply an incoming ingest message to the API.
|
|
4209
|
+
* - snapshot: rebuild state from nodes/edges (clears prior history)
|
|
4210
|
+
* - update: apply incremental update
|
|
4211
|
+
* - history: initialize from a set of frames (clears prior history)
|
|
4212
|
+
*/
|
|
4213
|
+
async apply(msg) {
|
|
4214
|
+
switch (msg.type) {
|
|
4215
|
+
case "snapshot": {
|
|
4216
|
+
await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
|
|
4217
|
+
break;
|
|
4218
|
+
}
|
|
4219
|
+
case "update": {
|
|
4220
|
+
await this.api.update((u) => {
|
|
4221
|
+
if (msg.addNodes) u.addNodes(...msg.addNodes);
|
|
4222
|
+
if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
|
|
4223
|
+
if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
|
|
4224
|
+
if (msg.addEdges) u.addEdges(...msg.addEdges);
|
|
4225
|
+
if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
|
|
4226
|
+
if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
|
|
4227
|
+
if (msg.description) u.describe(msg.description);
|
|
4228
|
+
});
|
|
4229
|
+
break;
|
|
4230
|
+
}
|
|
4231
|
+
case "history": {
|
|
4232
|
+
await this.api.replaceHistory(msg.frames);
|
|
4233
|
+
break;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
};
|
|
4238
|
+
|
|
4239
|
+
// src/api/sources/WebSocketSource.ts
|
|
4240
|
+
var WebSocketSource = class {
|
|
4241
|
+
url;
|
|
4242
|
+
ws = null;
|
|
4243
|
+
onMessage;
|
|
4244
|
+
onStatus;
|
|
4245
|
+
reconnectMs;
|
|
4246
|
+
closedByUser = false;
|
|
4247
|
+
connectStartTime = null;
|
|
4248
|
+
totalTimeoutMs = 1e4;
|
|
4249
|
+
totalTimeoutTimer = null;
|
|
4250
|
+
constructor(url, onMessage, onStatus, reconnectMs = 1500) {
|
|
4251
|
+
this.url = url;
|
|
4252
|
+
this.onMessage = onMessage;
|
|
4253
|
+
this.onStatus = onStatus;
|
|
4254
|
+
this.reconnectMs = reconnectMs;
|
|
4255
|
+
}
|
|
4256
|
+
connect() {
|
|
4257
|
+
this.closedByUser = false;
|
|
4258
|
+
this.connectStartTime = Date.now();
|
|
4259
|
+
this.startTotalTimeout();
|
|
4260
|
+
this.open();
|
|
4261
|
+
}
|
|
4262
|
+
disconnect() {
|
|
4263
|
+
this.closedByUser = true;
|
|
4264
|
+
this.clearTotalTimeout();
|
|
4265
|
+
if (this.ws) {
|
|
4266
|
+
try {
|
|
4267
|
+
this.ws.close();
|
|
4268
|
+
} catch {
|
|
4269
|
+
}
|
|
4270
|
+
this.ws = null;
|
|
4271
|
+
}
|
|
4272
|
+
this.onStatus?.("closed");
|
|
4273
|
+
}
|
|
4274
|
+
startTotalTimeout() {
|
|
4275
|
+
this.clearTotalTimeout();
|
|
4276
|
+
this.totalTimeoutTimer = window.setTimeout(() => {
|
|
4277
|
+
if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
|
|
4278
|
+
this.closedByUser = true;
|
|
4279
|
+
if (this.ws) {
|
|
4280
|
+
try {
|
|
4281
|
+
this.ws.close();
|
|
4282
|
+
} catch {
|
|
4283
|
+
}
|
|
4284
|
+
this.ws = null;
|
|
4285
|
+
}
|
|
4286
|
+
this.clearTotalTimeout();
|
|
4287
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4288
|
+
}
|
|
4289
|
+
}, this.totalTimeoutMs);
|
|
4290
|
+
}
|
|
4291
|
+
clearTotalTimeout() {
|
|
4292
|
+
if (this.totalTimeoutTimer !== null) {
|
|
4293
|
+
clearTimeout(this.totalTimeoutTimer);
|
|
4294
|
+
this.totalTimeoutTimer = null;
|
|
4295
|
+
}
|
|
4296
|
+
this.connectStartTime = null;
|
|
4297
|
+
}
|
|
4298
|
+
open() {
|
|
4299
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4300
|
+
if (!this.closedByUser) {
|
|
4301
|
+
this.closedByUser = true;
|
|
4302
|
+
this.clearTotalTimeout();
|
|
4303
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4304
|
+
}
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
this.onStatus?.(this.ws ? "reconnecting" : "connecting");
|
|
4308
|
+
const ws = new WebSocket(this.url);
|
|
4309
|
+
this.ws = ws;
|
|
4310
|
+
ws.onopen = () => {
|
|
4311
|
+
this.clearTotalTimeout();
|
|
4312
|
+
this.onStatus?.("connected");
|
|
4313
|
+
};
|
|
4314
|
+
ws.onerror = (e) => {
|
|
4315
|
+
this.onStatus?.("error", e);
|
|
4316
|
+
};
|
|
4317
|
+
ws.onclose = () => {
|
|
4318
|
+
if (this.closedByUser) {
|
|
4319
|
+
this.onStatus?.("closed");
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4323
|
+
this.closedByUser = true;
|
|
4324
|
+
this.clearTotalTimeout();
|
|
4325
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
this.onStatus?.("reconnecting");
|
|
4329
|
+
setTimeout(() => this.open(), this.reconnectMs);
|
|
4330
|
+
};
|
|
4331
|
+
ws.onmessage = (ev) => {
|
|
4332
|
+
const data = typeof ev.data === "string" ? ev.data : "";
|
|
4333
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4334
|
+
for (const line of lines) {
|
|
4335
|
+
try {
|
|
4336
|
+
const obj = JSON.parse(line);
|
|
4337
|
+
this.onMessage(obj);
|
|
4338
|
+
} catch {
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
};
|
|
4342
|
+
}
|
|
4343
|
+
};
|
|
4344
|
+
|
|
4345
|
+
// src/api/sources/FileSystemSource.ts
|
|
4346
|
+
var FileSystemSource = class {
|
|
4347
|
+
handle = null;
|
|
4348
|
+
onMessage;
|
|
4349
|
+
onStatus;
|
|
4350
|
+
timer = null;
|
|
4351
|
+
lastSize = 0;
|
|
4352
|
+
filename;
|
|
4353
|
+
intervalMs;
|
|
4354
|
+
constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
|
|
4355
|
+
this.onMessage = onMessage;
|
|
4356
|
+
this.onStatus = onStatus;
|
|
4357
|
+
this.filename = filename;
|
|
4358
|
+
this.intervalMs = intervalMs;
|
|
4359
|
+
}
|
|
4360
|
+
async openDirectory() {
|
|
4361
|
+
try {
|
|
4362
|
+
const dir = await window.showDirectoryPicker?.();
|
|
4363
|
+
if (!dir) throw new Error("File System Access not supported or cancelled");
|
|
4364
|
+
const handle = await dir.getFileHandle(this.filename, { create: false });
|
|
4365
|
+
this.handle = handle;
|
|
4366
|
+
this.onStatus?.("opened", { file: this.filename });
|
|
4367
|
+
this.lastSize = 0;
|
|
4368
|
+
this.startPolling();
|
|
4369
|
+
} catch (e) {
|
|
4370
|
+
this.onStatus?.("error", e);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
close() {
|
|
4374
|
+
if (this.timer) {
|
|
4375
|
+
window.clearInterval(this.timer);
|
|
4376
|
+
this.timer = null;
|
|
4377
|
+
}
|
|
4378
|
+
this.handle = null;
|
|
4379
|
+
this.onStatus?.("closed");
|
|
4380
|
+
}
|
|
4381
|
+
startPolling() {
|
|
4382
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4383
|
+
this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
|
|
4384
|
+
}
|
|
4385
|
+
async readNewLines() {
|
|
4386
|
+
try {
|
|
4387
|
+
if (!this.handle) return;
|
|
4388
|
+
this.onStatus?.("reading");
|
|
4389
|
+
const file = await this.handle.getFile();
|
|
4390
|
+
if (file.size === this.lastSize) return;
|
|
4391
|
+
const slice = await file.slice(this.lastSize).text();
|
|
4392
|
+
this.lastSize = file.size;
|
|
4393
|
+
const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4394
|
+
for (const line of lines) {
|
|
4395
|
+
try {
|
|
4396
|
+
const obj = JSON.parse(line);
|
|
4397
|
+
this.onMessage(obj);
|
|
4398
|
+
} catch {
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
} catch (e) {
|
|
4402
|
+
this.onStatus?.("error", e);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
};
|
|
4406
|
+
|
|
4407
|
+
// src/api/sources/FileSource.ts
|
|
4408
|
+
var FileSource = class {
|
|
4409
|
+
url;
|
|
4410
|
+
onMessage;
|
|
4411
|
+
onStatus;
|
|
4412
|
+
timer = null;
|
|
4413
|
+
lastETag = null;
|
|
4414
|
+
lastContent = "";
|
|
4415
|
+
intervalMs = 1e3;
|
|
4416
|
+
closed = false;
|
|
4417
|
+
constructor(url, onMessage, onStatus, intervalMs = 1e3) {
|
|
4418
|
+
this.url = url;
|
|
4419
|
+
this.onMessage = onMessage;
|
|
4420
|
+
this.onStatus = onStatus;
|
|
4421
|
+
this.intervalMs = intervalMs;
|
|
4422
|
+
}
|
|
4423
|
+
async connect() {
|
|
4424
|
+
this.closed = false;
|
|
4425
|
+
this.lastETag = null;
|
|
4426
|
+
this.lastContent = "";
|
|
4427
|
+
this.onStatus?.("opened");
|
|
4428
|
+
this.startPolling();
|
|
4429
|
+
}
|
|
4430
|
+
close() {
|
|
4431
|
+
this.closed = true;
|
|
4432
|
+
if (this.timer) {
|
|
4433
|
+
window.clearInterval(this.timer);
|
|
4434
|
+
this.timer = null;
|
|
4435
|
+
}
|
|
4436
|
+
this.onStatus?.("closed");
|
|
4437
|
+
}
|
|
4438
|
+
startPolling() {
|
|
4439
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4440
|
+
this.timer = window.setInterval(() => this.poll(), this.intervalMs);
|
|
4441
|
+
this.poll();
|
|
4442
|
+
}
|
|
4443
|
+
async poll() {
|
|
4444
|
+
if (this.closed) return;
|
|
4445
|
+
try {
|
|
4446
|
+
this.onStatus?.("reading");
|
|
4447
|
+
const headers = {};
|
|
4448
|
+
if (this.lastETag) {
|
|
4449
|
+
headers["If-None-Match"] = this.lastETag;
|
|
4450
|
+
}
|
|
4451
|
+
const response = await fetch(this.url, { headers });
|
|
4452
|
+
if (response.status === 304) {
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
if (!response.ok) {
|
|
4456
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4457
|
+
}
|
|
4458
|
+
const etag = response.headers.get("ETag");
|
|
4459
|
+
if (etag) {
|
|
4460
|
+
this.lastETag = etag;
|
|
4461
|
+
}
|
|
4462
|
+
const content = await response.text();
|
|
4463
|
+
if (content === this.lastContent) {
|
|
4464
|
+
return;
|
|
4465
|
+
}
|
|
4466
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4467
|
+
const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4468
|
+
const newLines = lines.slice(lastContentLines.length);
|
|
4469
|
+
for (const line of newLines) {
|
|
4470
|
+
try {
|
|
4471
|
+
const obj = JSON.parse(line);
|
|
4472
|
+
this.onMessage(obj);
|
|
4473
|
+
} catch {
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
this.lastContent = content;
|
|
4477
|
+
} catch (e) {
|
|
4478
|
+
this.onStatus?.("error", e);
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
};
|
|
4482
|
+
|
|
4483
|
+
// src/playground/playground.ts
|
|
4484
|
+
var import_styles2 = __toESM(require("./styles.css?raw"), 1);
|
|
4485
|
+
var Playground = class {
|
|
4486
|
+
options;
|
|
4487
|
+
rootElement;
|
|
4488
|
+
currentExample;
|
|
4489
|
+
examples;
|
|
4490
|
+
currentGraph = null;
|
|
4491
|
+
ingest = null;
|
|
4492
|
+
isEditable = false;
|
|
4493
|
+
wsSource = null;
|
|
4494
|
+
fsSource = null;
|
|
4495
|
+
fileSource = null;
|
|
4496
|
+
wsStatus = "disconnected";
|
|
4497
|
+
fsStatus = "disconnected";
|
|
4498
|
+
fileStatus = "disconnected";
|
|
4499
|
+
activeSourceType = null;
|
|
4500
|
+
wsUrl = "ws://localhost:8787";
|
|
4501
|
+
sourceModal = null;
|
|
4502
|
+
helpOverlay = null;
|
|
4503
|
+
exampleList;
|
|
4504
|
+
graphContainerId;
|
|
4505
|
+
constructor(options) {
|
|
4506
|
+
this.options = options;
|
|
4507
|
+
this.examples = { ...options.examples };
|
|
4508
|
+
this.exampleList = Object.keys(this.examples);
|
|
4509
|
+
this.currentExample = options.defaultExample || this.exampleList[0];
|
|
4510
|
+
this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
|
|
4511
|
+
if (typeof options.root === "string") {
|
|
4512
|
+
const el = document.getElementById(options.root);
|
|
4513
|
+
if (!el) throw new Error(`Element with id "${options.root}" not found`);
|
|
4514
|
+
this.rootElement = el;
|
|
4515
|
+
} else {
|
|
4516
|
+
this.rootElement = options.root;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
async init() {
|
|
4520
|
+
this.injectStyles();
|
|
4521
|
+
this.createDOM();
|
|
4522
|
+
this.setupEventListeners();
|
|
4523
|
+
await this.renderGraph();
|
|
4524
|
+
this.updateSourceIcon();
|
|
4525
|
+
this.connectExampleSource();
|
|
4526
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4527
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4528
|
+
}
|
|
4529
|
+
injectStyles() {
|
|
4530
|
+
if (!document.getElementById("g3p-playground-styles")) {
|
|
4531
|
+
const styleEl = document.createElement("style");
|
|
4532
|
+
styleEl.id = "g3p-playground-styles";
|
|
4533
|
+
styleEl.textContent = import_styles2.default;
|
|
4534
|
+
document.head.appendChild(styleEl);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
createDOM() {
|
|
4538
|
+
const exampleList = this.exampleList.map((key, i) => {
|
|
4539
|
+
const example = this.examples[key];
|
|
4540
|
+
const isActive = i === 0 || key === this.currentExample;
|
|
4541
|
+
return `
|
|
4542
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
4543
|
+
${example.name}
|
|
4544
|
+
</button>
|
|
4545
|
+
`;
|
|
4546
|
+
}).join("");
|
|
4547
|
+
this.rootElement.innerHTML = `
|
|
4548
|
+
<main class="playground">
|
|
4549
|
+
<div class="sidebar">
|
|
4550
|
+
<h2>Examples</h2>
|
|
4551
|
+
<div class="example-list">
|
|
4552
|
+
${exampleList}
|
|
4553
|
+
</div>
|
|
4554
|
+
|
|
4555
|
+
<h2>Options</h2>
|
|
4556
|
+
<div class="options">
|
|
4557
|
+
<div class="option-group">
|
|
4558
|
+
<label>Orientation</label>
|
|
4559
|
+
<select id="orientation">
|
|
4560
|
+
<option value="TB">Top to Bottom</option>
|
|
4561
|
+
<option value="BT">Bottom to Top</option>
|
|
4562
|
+
<option value="LR">Left to Right</option>
|
|
4563
|
+
<option value="RL">Right to Left</option>
|
|
4564
|
+
</select>
|
|
4565
|
+
</div>
|
|
4566
|
+
|
|
4567
|
+
<div class="option-group">
|
|
4568
|
+
<label>Port Style</label>
|
|
4569
|
+
<select id="portStyle">
|
|
4570
|
+
<option value="outside">Outside</option>
|
|
4571
|
+
<option value="inside">Inside</option>
|
|
4572
|
+
</select>
|
|
4573
|
+
</div>
|
|
4574
|
+
|
|
4575
|
+
<div class="option-group">
|
|
4576
|
+
<label>
|
|
4577
|
+
<input type="checkbox" id="portLabelRotate" />
|
|
4578
|
+
Rotate Port Labels
|
|
4579
|
+
</label>
|
|
4580
|
+
</div>
|
|
4581
|
+
|
|
4582
|
+
<div class="option-group">
|
|
4583
|
+
<label>Theme</label>
|
|
4584
|
+
<select id="colorMode">
|
|
4585
|
+
<option value="system">System</option>
|
|
4586
|
+
<option value="light">Light</option>
|
|
4587
|
+
<option value="dark">Dark</option>
|
|
4588
|
+
</select>
|
|
4589
|
+
</div>
|
|
4590
|
+
</div>
|
|
4591
|
+
</div>
|
|
4592
|
+
|
|
4593
|
+
<div class="graph-area">
|
|
4594
|
+
<div class="graph-toolbar">
|
|
4595
|
+
<div class="nav-controls">
|
|
4596
|
+
<button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
|
|
4597
|
+
<button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
|
|
4598
|
+
<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>
|
|
4599
|
+
<button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
|
|
4600
|
+
<button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
|
|
4601
|
+
</div>
|
|
4602
|
+
<div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
|
|
4603
|
+
<button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
|
|
4604
|
+
</div>
|
|
4605
|
+
<button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
|
|
4606
|
+
<button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
|
|
4607
|
+
<button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
|
|
4608
|
+
</div>
|
|
4609
|
+
<div class="graph-container" id="${this.graphContainerId}"></div>
|
|
4610
|
+
</div>
|
|
4611
|
+
</main>
|
|
4612
|
+
`;
|
|
4613
|
+
}
|
|
4614
|
+
setupEventListeners() {
|
|
4615
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
|
|
4616
|
+
btn.addEventListener("click", () => {
|
|
4617
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
4618
|
+
btn.classList.add("active");
|
|
4619
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
4620
|
+
this.renderGraph();
|
|
4621
|
+
this.connectExampleSource();
|
|
4622
|
+
});
|
|
4623
|
+
});
|
|
4624
|
+
this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
|
|
4625
|
+
el.addEventListener("change", () => {
|
|
4626
|
+
if (el.id === "colorMode" && this.currentGraph) {
|
|
4627
|
+
const mode = el.value;
|
|
4628
|
+
this.currentGraph.setColorMode(mode);
|
|
4629
|
+
} else {
|
|
4630
|
+
this.renderGraph();
|
|
4631
|
+
}
|
|
4632
|
+
});
|
|
4633
|
+
});
|
|
4634
|
+
this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
|
|
4635
|
+
this.currentGraph?.nav("first");
|
|
4636
|
+
this.updateHistoryLabel();
|
|
4637
|
+
});
|
|
4638
|
+
this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
|
|
4639
|
+
this.currentGraph?.nav("prev");
|
|
4640
|
+
this.updateHistoryLabel();
|
|
4641
|
+
});
|
|
4642
|
+
this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
|
|
4643
|
+
this.currentGraph?.nav("next");
|
|
4644
|
+
this.updateHistoryLabel();
|
|
4645
|
+
});
|
|
4646
|
+
this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
|
|
4647
|
+
this.currentGraph?.nav("last");
|
|
4648
|
+
this.updateHistoryLabel();
|
|
4649
|
+
});
|
|
4650
|
+
this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
|
|
4651
|
+
this.currentGraph?.rebuild();
|
|
4652
|
+
});
|
|
4653
|
+
this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
|
|
4654
|
+
this.isEditable = !this.isEditable;
|
|
4655
|
+
const btn = this.rootElement.querySelector("#edit-toggle");
|
|
4656
|
+
if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
|
|
4657
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4658
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4659
|
+
try {
|
|
4660
|
+
this.currentGraph?.setEditable?.(this.isEditable);
|
|
4661
|
+
} catch {
|
|
4662
|
+
}
|
|
4663
|
+
});
|
|
4664
|
+
this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
|
|
4665
|
+
const sourceIconBtn = this.rootElement.querySelector("#source-icon");
|
|
4666
|
+
if (sourceIconBtn) {
|
|
4667
|
+
sourceIconBtn.addEventListener("click", (e) => {
|
|
4668
|
+
e.preventDefault();
|
|
4669
|
+
e.stopPropagation();
|
|
4670
|
+
this.openSourceModal();
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
document.addEventListener("keydown", (e) => {
|
|
4674
|
+
if (!this.currentGraph) return;
|
|
4675
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
|
|
4676
|
+
switch (e.key) {
|
|
4677
|
+
case "Home":
|
|
4678
|
+
this.currentGraph.nav("first");
|
|
4679
|
+
this.updateHistoryLabel();
|
|
4680
|
+
break;
|
|
4681
|
+
case "End":
|
|
4682
|
+
this.currentGraph.nav("last");
|
|
4683
|
+
this.updateHistoryLabel();
|
|
4684
|
+
break;
|
|
4685
|
+
case "ArrowLeft":
|
|
4686
|
+
this.currentGraph.nav("prev");
|
|
4687
|
+
this.updateHistoryLabel();
|
|
4688
|
+
break;
|
|
4689
|
+
case "ArrowRight":
|
|
4690
|
+
this.currentGraph.nav("next");
|
|
4691
|
+
this.updateHistoryLabel();
|
|
4692
|
+
break;
|
|
4693
|
+
}
|
|
4694
|
+
});
|
|
4695
|
+
}
|
|
4696
|
+
getResolvedColorMode() {
|
|
4697
|
+
const mode = this.rootElement.querySelector("#colorMode")?.value;
|
|
4698
|
+
if (mode === "system") {
|
|
4699
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4700
|
+
}
|
|
4701
|
+
return mode;
|
|
4702
|
+
}
|
|
4703
|
+
getOptions(exampleOptions) {
|
|
4704
|
+
const orientation = this.rootElement.querySelector("#orientation")?.value;
|
|
4705
|
+
return {
|
|
4706
|
+
graph: { orientation },
|
|
4707
|
+
canvas: {
|
|
4708
|
+
width: "100%",
|
|
4709
|
+
height: "100%",
|
|
4710
|
+
colorMode: this.getResolvedColorMode(),
|
|
4711
|
+
editable: this.isEditable,
|
|
4712
|
+
...exampleOptions?.canvas
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
async renderGraph() {
|
|
4717
|
+
const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
|
|
4718
|
+
if (!container) return;
|
|
4719
|
+
this.currentGraph?.destroy();
|
|
4720
|
+
this.currentGraph = null;
|
|
4721
|
+
container.innerHTML = "";
|
|
4722
|
+
const example = this.examples[this.currentExample];
|
|
4723
|
+
const options = this.getOptions(example.options);
|
|
4724
|
+
try {
|
|
4725
|
+
this.currentGraph = await graph({
|
|
4726
|
+
root: this.graphContainerId,
|
|
4727
|
+
nodes: example.nodes,
|
|
4728
|
+
edges: example.edges,
|
|
4729
|
+
options,
|
|
4730
|
+
events: {
|
|
4731
|
+
historyChange: () => this.updateHistoryLabel()
|
|
4732
|
+
}
|
|
4733
|
+
});
|
|
4734
|
+
this.ingest = new Ingest(this.currentGraph);
|
|
4735
|
+
this.updateHistoryLabel();
|
|
4736
|
+
} catch (e) {
|
|
4737
|
+
console.error("Failed to render graph:", e);
|
|
4738
|
+
container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
updateHistoryLabel() {
|
|
4742
|
+
const label = this.rootElement.querySelector("#history-label");
|
|
4743
|
+
if (!label || !this.currentGraph) return;
|
|
4744
|
+
try {
|
|
4745
|
+
const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
|
|
4746
|
+
const len = this.currentGraph.getHistoryLength?.() ?? 1;
|
|
4747
|
+
label.textContent = `${idx + 1} / ${len}`;
|
|
4748
|
+
} catch {
|
|
4749
|
+
label.textContent = "\u2014 / \u2014";
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
connectExampleSource() {
|
|
4753
|
+
const example = this.examples[this.currentExample];
|
|
4754
|
+
if (!example.source) {
|
|
4755
|
+
this.disconnectAllSources();
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
this.disconnectAllSources();
|
|
4759
|
+
if (example.source.type === "websocket") {
|
|
4760
|
+
this.wsUrl = example.source.url;
|
|
4761
|
+
this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
4762
|
+
this.wsSource.connect();
|
|
4763
|
+
} else if (example.source.type === "file") {
|
|
4764
|
+
this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
|
|
4765
|
+
this.fileSource.connect();
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
disconnectAllSources() {
|
|
4769
|
+
this.wsSource?.disconnect();
|
|
4770
|
+
this.fsSource?.close();
|
|
4771
|
+
this.fileSource?.close();
|
|
4772
|
+
this.wsSource = null;
|
|
4773
|
+
this.fsSource = null;
|
|
4774
|
+
this.fileSource = null;
|
|
4775
|
+
this.activeSourceType = null;
|
|
4776
|
+
this.wsStatus = "disconnected";
|
|
4777
|
+
this.fsStatus = "disconnected";
|
|
4778
|
+
this.fileStatus = "disconnected";
|
|
4779
|
+
this.updateSourceIcon();
|
|
4780
|
+
}
|
|
4781
|
+
openHelp() {
|
|
4782
|
+
if (!this.helpOverlay) {
|
|
4783
|
+
this.helpOverlay = document.createElement("div");
|
|
4784
|
+
this.helpOverlay.className = "modal-overlay";
|
|
4785
|
+
this.helpOverlay.innerHTML = `
|
|
4786
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
4787
|
+
<div class="modal-header">
|
|
4788
|
+
<h3 id="help-title">Editing the Graph</h3>
|
|
4789
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4790
|
+
</div>
|
|
4791
|
+
<div class="modal-body">
|
|
4792
|
+
<p>Here's how to edit the graph:</p>
|
|
4793
|
+
<ul>
|
|
4794
|
+
<li><strong>Enable editing</strong>: Click "Edit"</li>
|
|
4795
|
+
<li><strong>Add a node</strong>: Double\u2011click an empty area</li>
|
|
4796
|
+
<li><strong>Edit a node</strong>: Double\u2011click a node</li>
|
|
4797
|
+
<li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
|
|
4798
|
+
<li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
|
|
4799
|
+
<li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
|
|
4800
|
+
<li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
|
|
4801
|
+
</ul>
|
|
4802
|
+
<p>When you're done, click "Done" to lock the canvas.</p>
|
|
4803
|
+
</div>
|
|
4804
|
+
</div>
|
|
4805
|
+
`;
|
|
4806
|
+
document.body.appendChild(this.helpOverlay);
|
|
4807
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
4808
|
+
const target = e.target;
|
|
4809
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4810
|
+
this.closeHelp();
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
this.helpOverlay.style.display = "flex";
|
|
4815
|
+
}
|
|
4816
|
+
closeHelp() {
|
|
4817
|
+
if (this.helpOverlay) this.helpOverlay.style.display = "none";
|
|
4818
|
+
}
|
|
4819
|
+
handleIngestMessage = async (msg) => {
|
|
4820
|
+
if (!this.ingest) return;
|
|
4821
|
+
await this.ingest.apply(msg);
|
|
4822
|
+
};
|
|
4823
|
+
updateSourceIcon() {
|
|
4824
|
+
const iconBtn = this.rootElement.querySelector("#source-icon");
|
|
4825
|
+
if (!iconBtn) return;
|
|
4826
|
+
iconBtn.classList.remove("active", "connecting", "error");
|
|
4827
|
+
const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
|
|
4828
|
+
const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
|
|
4829
|
+
const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
|
|
4830
|
+
let icon = "\u{1F4E1}";
|
|
4831
|
+
if (this.activeSourceType === "folder") {
|
|
4832
|
+
icon = "\u{1F4C1}";
|
|
4833
|
+
} else if (this.activeSourceType === "file") {
|
|
4834
|
+
icon = "\u{1F4C4}";
|
|
4835
|
+
}
|
|
4836
|
+
if (isConnected) {
|
|
4837
|
+
iconBtn.classList.add("active");
|
|
4838
|
+
iconBtn.textContent = icon;
|
|
4839
|
+
} else if (isConnecting) {
|
|
4840
|
+
iconBtn.classList.add("connecting");
|
|
4841
|
+
iconBtn.textContent = icon;
|
|
4842
|
+
} else if (hasError) {
|
|
4843
|
+
iconBtn.classList.add("error");
|
|
4844
|
+
iconBtn.textContent = icon;
|
|
4845
|
+
} else {
|
|
4846
|
+
iconBtn.textContent = "\u{1F4E1}";
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
updateWsStatus = (status, detail) => {
|
|
4850
|
+
if (status === "connecting" || status === "reconnecting") {
|
|
4851
|
+
this.wsStatus = "connecting";
|
|
4852
|
+
this.activeSourceType = "ws";
|
|
4853
|
+
} else if (status === "connected") {
|
|
4854
|
+
this.wsStatus = "connected";
|
|
4855
|
+
this.activeSourceType = "ws";
|
|
4856
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4857
|
+
this.fsSource.close();
|
|
4858
|
+
}
|
|
4859
|
+
if (this.fileSource && this.fileStatus === "connected") {
|
|
4860
|
+
this.fileSource.close();
|
|
4861
|
+
}
|
|
4862
|
+
} else if (status === "error") {
|
|
4863
|
+
this.wsStatus = "error";
|
|
4864
|
+
} else {
|
|
4865
|
+
this.wsStatus = "disconnected";
|
|
4866
|
+
if (this.activeSourceType === "ws") {
|
|
4867
|
+
this.activeSourceType = null;
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
4870
|
+
this.updateSourceIcon();
|
|
4871
|
+
this.updateSourceModal();
|
|
4872
|
+
};
|
|
4873
|
+
updateFsStatus = (status, detail) => {
|
|
4874
|
+
if (status === "opened") {
|
|
4875
|
+
this.fsStatus = "opening";
|
|
4876
|
+
this.activeSourceType = "folder";
|
|
4877
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4878
|
+
this.wsSource.disconnect();
|
|
4879
|
+
}
|
|
4880
|
+
} else if (status === "reading") {
|
|
4881
|
+
this.fsStatus = "connected";
|
|
4882
|
+
this.activeSourceType = "folder";
|
|
4883
|
+
} else if (status === "error") {
|
|
4884
|
+
this.fsStatus = "error";
|
|
4885
|
+
} else if (status === "closed") {
|
|
4886
|
+
this.fsStatus = "disconnected";
|
|
4887
|
+
if (this.activeSourceType === "folder") {
|
|
4888
|
+
this.activeSourceType = null;
|
|
4889
|
+
}
|
|
4890
|
+
} else {
|
|
4891
|
+
this.fsStatus = "disconnected";
|
|
4892
|
+
}
|
|
4893
|
+
this.updateSourceIcon();
|
|
4894
|
+
this.updateSourceModal();
|
|
4895
|
+
};
|
|
4896
|
+
updateFileStatus = (status, detail) => {
|
|
4897
|
+
if (status === "opened") {
|
|
4898
|
+
this.fileStatus = "connecting";
|
|
4899
|
+
this.activeSourceType = "file";
|
|
4900
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4901
|
+
this.wsSource.disconnect();
|
|
4902
|
+
}
|
|
4903
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4904
|
+
this.fsSource.close();
|
|
4905
|
+
}
|
|
4906
|
+
} else if (status === "reading") {
|
|
4907
|
+
this.fileStatus = "connected";
|
|
4908
|
+
this.activeSourceType = "file";
|
|
4909
|
+
} else if (status === "error") {
|
|
4910
|
+
this.fileStatus = "error";
|
|
4911
|
+
} else if (status === "closed") {
|
|
4912
|
+
this.fileStatus = "disconnected";
|
|
4913
|
+
if (this.activeSourceType === "file") {
|
|
4914
|
+
this.activeSourceType = null;
|
|
4915
|
+
}
|
|
4916
|
+
} else {
|
|
4917
|
+
this.fileStatus = "disconnected";
|
|
4918
|
+
}
|
|
4919
|
+
this.updateSourceIcon();
|
|
4920
|
+
this.updateSourceModal();
|
|
4921
|
+
};
|
|
4922
|
+
createSourceModal() {
|
|
4923
|
+
if (this.sourceModal) return this.sourceModal;
|
|
4924
|
+
this.sourceModal = document.createElement("div");
|
|
4925
|
+
this.sourceModal.className = "modal-overlay";
|
|
4926
|
+
this.sourceModal.style.display = "none";
|
|
4927
|
+
this.sourceModal.innerHTML = `
|
|
4928
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
|
|
4929
|
+
<div class="modal-header">
|
|
4930
|
+
<h3 id="source-modal-title">Data Source Connection</h3>
|
|
4931
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4932
|
+
</div>
|
|
4933
|
+
<div class="modal-body">
|
|
4934
|
+
<div class="source-type-selector">
|
|
4935
|
+
<button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
|
|
4936
|
+
<button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
|
|
4937
|
+
</div>
|
|
4938
|
+
|
|
4939
|
+
<div class="source-controls" data-source="ws">
|
|
4940
|
+
<div class="form-group">
|
|
4941
|
+
<label for="source-modal-url">WebSocket URL</label>
|
|
4942
|
+
<input type="text" id="source-modal-url" value="${this.wsUrl}" />
|
|
4943
|
+
</div>
|
|
4944
|
+
<div class="button-group">
|
|
4945
|
+
<button id="source-modal-connect-ws" class="primary">Connect</button>
|
|
4946
|
+
<button id="source-modal-disconnect-ws">Disconnect</button>
|
|
4947
|
+
<button id="source-modal-change-ws">Change Connection</button>
|
|
4948
|
+
</div>
|
|
4949
|
+
</div>
|
|
4950
|
+
|
|
4951
|
+
<div class="source-controls" data-source="folder">
|
|
4952
|
+
<div class="form-group">
|
|
4953
|
+
<label>File System Source</label>
|
|
4954
|
+
<p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
|
|
4955
|
+
Select a directory containing a graph.ndjson file to watch for changes.
|
|
4956
|
+
</p>
|
|
4957
|
+
</div>
|
|
4958
|
+
<div class="button-group">
|
|
4959
|
+
<button id="source-modal-connect-folder" class="primary">Open Folder</button>
|
|
4960
|
+
<button id="source-modal-disconnect-folder">Disconnect</button>
|
|
4961
|
+
</div>
|
|
4962
|
+
</div>
|
|
4963
|
+
|
|
4964
|
+
<div id="source-modal-status"></div>
|
|
4965
|
+
</div>
|
|
4966
|
+
</div>
|
|
4967
|
+
`;
|
|
4968
|
+
document.body.appendChild(this.sourceModal);
|
|
4969
|
+
this.sourceModal.addEventListener("click", (e) => {
|
|
4970
|
+
const target = e.target;
|
|
4971
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4972
|
+
this.closeSourceModal();
|
|
4973
|
+
}
|
|
4974
|
+
});
|
|
4975
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4976
|
+
btn.addEventListener("click", () => {
|
|
4977
|
+
const sourceType = btn.getAttribute("data-source");
|
|
4978
|
+
if (sourceType) {
|
|
4979
|
+
this.selectSourceType(sourceType);
|
|
4980
|
+
}
|
|
4981
|
+
});
|
|
4982
|
+
});
|
|
4983
|
+
document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
|
|
4984
|
+
document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
|
|
4985
|
+
document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
|
|
4986
|
+
document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
|
|
4987
|
+
document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
|
|
4988
|
+
document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
|
|
4989
|
+
if (e.key === "Enter" && this.wsStatus !== "connected") {
|
|
4990
|
+
this.handleConnect();
|
|
4991
|
+
}
|
|
4992
|
+
});
|
|
4993
|
+
return this.sourceModal;
|
|
4994
|
+
}
|
|
4995
|
+
selectSourceType(type, skipUpdate = false) {
|
|
4996
|
+
if (!this.sourceModal) return;
|
|
4997
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4998
|
+
if (btn.getAttribute("data-source") === type) {
|
|
4999
|
+
btn.classList.add("active");
|
|
5000
|
+
} else {
|
|
5001
|
+
btn.classList.remove("active");
|
|
5002
|
+
}
|
|
5003
|
+
});
|
|
5004
|
+
this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
|
|
5005
|
+
if (controls.getAttribute("data-source") === type) {
|
|
5006
|
+
controls.classList.add("active");
|
|
5007
|
+
} else {
|
|
5008
|
+
controls.classList.remove("active");
|
|
5009
|
+
}
|
|
5010
|
+
});
|
|
5011
|
+
if (!skipUpdate) {
|
|
5012
|
+
this.updateSourceModalContent();
|
|
5013
|
+
}
|
|
5014
|
+
}
|
|
5015
|
+
updateSourceModalContent() {
|
|
5016
|
+
if (!this.sourceModal) return;
|
|
5017
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5018
|
+
const connectWsBtn = document.getElementById("source-modal-connect-ws");
|
|
5019
|
+
const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
|
|
5020
|
+
const changeWsBtn = document.getElementById("source-modal-change-ws");
|
|
5021
|
+
if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
|
|
5022
|
+
const isWsConnected = this.wsStatus === "connected";
|
|
5023
|
+
const isWsConnecting = this.wsStatus === "connecting";
|
|
5024
|
+
connectWsBtn.disabled = isWsConnected || isWsConnecting;
|
|
5025
|
+
disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
5026
|
+
changeWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
5027
|
+
urlInput.disabled = isWsConnecting;
|
|
5028
|
+
}
|
|
5029
|
+
const connectFolderBtn = document.getElementById("source-modal-connect-folder");
|
|
5030
|
+
const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
|
|
5031
|
+
if (connectFolderBtn && disconnectFolderBtn) {
|
|
5032
|
+
const isFolderConnected = this.fsStatus === "connected";
|
|
5033
|
+
const isFolderOpening = this.fsStatus === "opening";
|
|
5034
|
+
connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
|
|
5035
|
+
disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
|
|
5036
|
+
}
|
|
5037
|
+
const statusDiv = document.getElementById("source-modal-status");
|
|
5038
|
+
if (!statusDiv) return;
|
|
5039
|
+
const currentUrl = urlInput?.value || this.wsUrl;
|
|
5040
|
+
statusDiv.innerHTML = "";
|
|
5041
|
+
if (this.activeSourceType === "ws") {
|
|
5042
|
+
if (this.wsStatus === "connecting") {
|
|
5043
|
+
statusDiv.innerHTML = `
|
|
5044
|
+
<div class="status-message info">
|
|
5045
|
+
<span class="loading-spinner"></span>
|
|
5046
|
+
Connecting to ${currentUrl}...
|
|
5047
|
+
</div>
|
|
5048
|
+
`;
|
|
5049
|
+
} else if (this.wsStatus === "connected") {
|
|
5050
|
+
statusDiv.innerHTML = `
|
|
5051
|
+
<div class="status-message success">
|
|
5052
|
+
\u2713 Connected to ${currentUrl}
|
|
5053
|
+
</div>
|
|
5054
|
+
`;
|
|
5055
|
+
} else if (this.wsStatus === "error") {
|
|
5056
|
+
statusDiv.innerHTML = `
|
|
5057
|
+
<div class="status-message error">
|
|
5058
|
+
\u2717 Connection error. Please check the URL and try again.
|
|
5059
|
+
</div>
|
|
5060
|
+
`;
|
|
5061
|
+
} else {
|
|
5062
|
+
statusDiv.innerHTML = `
|
|
5063
|
+
<div class="status-message info">
|
|
5064
|
+
Not connected
|
|
5065
|
+
</div>
|
|
5066
|
+
`;
|
|
5067
|
+
}
|
|
5068
|
+
} else if (this.activeSourceType === "folder") {
|
|
5069
|
+
if (this.fsStatus === "opening") {
|
|
5070
|
+
statusDiv.innerHTML = `
|
|
5071
|
+
<div class="status-message info">
|
|
5072
|
+
<span class="loading-spinner"></span>
|
|
5073
|
+
Opening folder...
|
|
5074
|
+
</div>
|
|
5075
|
+
`;
|
|
5076
|
+
} else if (this.fsStatus === "connected") {
|
|
5077
|
+
statusDiv.innerHTML = `
|
|
5078
|
+
<div class="status-message success">
|
|
5079
|
+
\u2713 Folder connected and watching for changes
|
|
5080
|
+
</div>
|
|
5081
|
+
`;
|
|
5082
|
+
} else if (this.fsStatus === "error") {
|
|
5083
|
+
statusDiv.innerHTML = `
|
|
5084
|
+
<div class="status-message error">
|
|
5085
|
+
\u2717 Error opening folder. Please try again.
|
|
5086
|
+
</div>
|
|
5087
|
+
`;
|
|
5088
|
+
} else {
|
|
5089
|
+
statusDiv.innerHTML = `
|
|
5090
|
+
<div class="status-message info">
|
|
5091
|
+
Not connected
|
|
5092
|
+
</div>
|
|
5093
|
+
`;
|
|
5094
|
+
}
|
|
5095
|
+
} else if (this.activeSourceType === "file") {
|
|
5096
|
+
const example = this.examples[this.currentExample];
|
|
5097
|
+
const filePath = example.source?.type === "file" ? example.source.path : "";
|
|
5098
|
+
if (this.fileStatus === "connecting") {
|
|
5099
|
+
statusDiv.innerHTML = `
|
|
5100
|
+
<div class="status-message info">
|
|
5101
|
+
<span class="loading-spinner"></span>
|
|
5102
|
+
Connecting to ${filePath}...
|
|
5103
|
+
</div>
|
|
5104
|
+
`;
|
|
5105
|
+
} else if (this.fileStatus === "connected") {
|
|
5106
|
+
statusDiv.innerHTML = `
|
|
5107
|
+
<div class="status-message success">
|
|
5108
|
+
\u2713 Connected to ${filePath}
|
|
5109
|
+
</div>
|
|
5110
|
+
`;
|
|
5111
|
+
} else if (this.fileStatus === "error") {
|
|
5112
|
+
statusDiv.innerHTML = `
|
|
5113
|
+
<div class="status-message error">
|
|
5114
|
+
\u2717 Error loading file. Please check the path and try again.
|
|
5115
|
+
</div>
|
|
5116
|
+
`;
|
|
5117
|
+
} else {
|
|
5118
|
+
statusDiv.innerHTML = `
|
|
5119
|
+
<div class="status-message info">
|
|
5120
|
+
Not connected
|
|
5121
|
+
</div>
|
|
5122
|
+
`;
|
|
5123
|
+
}
|
|
5124
|
+
} else {
|
|
5125
|
+
statusDiv.innerHTML = `
|
|
5126
|
+
<div class="status-message info">
|
|
5127
|
+
Select a source type to connect
|
|
5128
|
+
</div>
|
|
5129
|
+
`;
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
updateSourceModal() {
|
|
5133
|
+
if (!this.sourceModal) return;
|
|
5134
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5135
|
+
this.selectSourceType(activeType, true);
|
|
5136
|
+
this.updateSourceModalContent();
|
|
5137
|
+
}
|
|
5138
|
+
openSourceModal() {
|
|
5139
|
+
this.createSourceModal();
|
|
5140
|
+
if (this.sourceModal) {
|
|
5141
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5142
|
+
if (urlInput) {
|
|
5143
|
+
urlInput.value = this.wsUrl;
|
|
5144
|
+
}
|
|
5145
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5146
|
+
this.selectSourceType(activeType);
|
|
5147
|
+
this.updateSourceModal();
|
|
5148
|
+
this.sourceModal.style.display = "flex";
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
closeSourceModal() {
|
|
5152
|
+
if (this.sourceModal) {
|
|
5153
|
+
this.sourceModal.style.display = "none";
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
handleConnect() {
|
|
5157
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5158
|
+
if (!urlInput) return;
|
|
5159
|
+
const url = urlInput.value.trim() || "ws://localhost:8787";
|
|
5160
|
+
this.wsUrl = url;
|
|
5161
|
+
if (this.wsSource) {
|
|
5162
|
+
this.wsSource.disconnect();
|
|
5163
|
+
}
|
|
5164
|
+
this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
5165
|
+
this.wsSource.connect();
|
|
5166
|
+
this.updateSourceModal();
|
|
5167
|
+
}
|
|
5168
|
+
handleDisconnect() {
|
|
5169
|
+
this.wsSource?.disconnect();
|
|
5170
|
+
this.updateSourceModal();
|
|
5171
|
+
}
|
|
5172
|
+
handleChangeConnection() {
|
|
5173
|
+
if (this.wsSource) {
|
|
5174
|
+
this.wsSource.disconnect();
|
|
5175
|
+
}
|
|
5176
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5177
|
+
if (urlInput) {
|
|
5178
|
+
urlInput.focus();
|
|
5179
|
+
urlInput.select();
|
|
5180
|
+
}
|
|
5181
|
+
this.updateSourceModal();
|
|
5182
|
+
}
|
|
5183
|
+
async handleOpenFolder() {
|
|
5184
|
+
if (!this.fsSource) {
|
|
5185
|
+
this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
|
|
5186
|
+
}
|
|
5187
|
+
this.updateSourceModal();
|
|
5188
|
+
await this.fsSource.openDirectory();
|
|
5189
|
+
}
|
|
5190
|
+
handleCloseFolder() {
|
|
5191
|
+
this.fsSource?.close();
|
|
5192
|
+
this.updateSourceModal();
|
|
5193
|
+
}
|
|
5194
|
+
/**
|
|
5195
|
+
* Add or update an example
|
|
5196
|
+
*/
|
|
5197
|
+
addExample(key, example) {
|
|
5198
|
+
this.examples[key] = example;
|
|
5199
|
+
this.updateExampleList();
|
|
5200
|
+
if (this.currentExample === key) {
|
|
5201
|
+
this.renderGraph();
|
|
5202
|
+
this.connectExampleSource();
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
/**
|
|
5206
|
+
* Remove an example
|
|
5207
|
+
*/
|
|
5208
|
+
removeExample(key) {
|
|
5209
|
+
delete this.examples[key];
|
|
5210
|
+
if (this.currentExample === key) {
|
|
5211
|
+
this.exampleList = Object.keys(this.examples);
|
|
5212
|
+
this.currentExample = this.exampleList[0] || "";
|
|
5213
|
+
if (this.currentExample) {
|
|
5214
|
+
this.renderGraph();
|
|
5215
|
+
this.connectExampleSource();
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
this.updateExampleList();
|
|
5219
|
+
}
|
|
5220
|
+
/**
|
|
5221
|
+
* Update the example list in the DOM
|
|
5222
|
+
*/
|
|
5223
|
+
updateExampleList() {
|
|
5224
|
+
this.exampleList = Object.keys(this.examples);
|
|
5225
|
+
const exampleListEl = this.rootElement.querySelector(".example-list");
|
|
5226
|
+
if (!exampleListEl) return;
|
|
5227
|
+
exampleListEl.innerHTML = this.exampleList.map((key, i) => {
|
|
5228
|
+
const example = this.examples[key];
|
|
5229
|
+
const isActive = key === this.currentExample;
|
|
5230
|
+
return `
|
|
5231
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
5232
|
+
${example.name}
|
|
5233
|
+
</button>
|
|
5234
|
+
`;
|
|
5235
|
+
}).join("");
|
|
5236
|
+
exampleListEl.querySelectorAll(".example-btn").forEach((btn) => {
|
|
5237
|
+
btn.addEventListener("click", () => {
|
|
5238
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
5239
|
+
btn.classList.add("active");
|
|
5240
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
5241
|
+
this.renderGraph();
|
|
5242
|
+
this.connectExampleSource();
|
|
5243
|
+
});
|
|
5244
|
+
});
|
|
5245
|
+
}
|
|
4078
5246
|
};
|
|
4079
5247
|
|
|
4080
5248
|
// src/index.ts
|
|
@@ -4086,5 +5254,11 @@ async function graph(args = { root: "app" }) {
|
|
|
4086
5254
|
var index_default = graph;
|
|
4087
5255
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4088
5256
|
0 && (module.exports = {
|
|
5257
|
+
FileSource,
|
|
5258
|
+
FileSystemSource,
|
|
5259
|
+
Ingest,
|
|
5260
|
+
Playground,
|
|
5261
|
+
Updater,
|
|
5262
|
+
WebSocketSource,
|
|
4089
5263
|
graph
|
|
4090
5264
|
});
|