@3plate/graph-core 0.1.5 → 0.1.6
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 +1059 -20
- package/dist/index.d.cts +198 -3
- package/dist/index.d.ts +198 -3
- package/dist/index.js +1054 -20
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,11 @@ 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
|
+
WebSocketSource: () => WebSocketSource,
|
|
33
38
|
default: () => index_default,
|
|
34
39
|
graph: () => graph
|
|
35
40
|
});
|
|
@@ -3557,26 +3562,50 @@ var Updater = class _Updater {
|
|
|
3557
3562
|
this.update.addNodes.push(node);
|
|
3558
3563
|
return this;
|
|
3559
3564
|
}
|
|
3565
|
+
addNodes(...nodes) {
|
|
3566
|
+
this.update.addNodes.push(...nodes);
|
|
3567
|
+
return this;
|
|
3568
|
+
}
|
|
3560
3569
|
deleteNode(node) {
|
|
3561
3570
|
this.update.removeNodes.push(node);
|
|
3562
3571
|
return this;
|
|
3563
3572
|
}
|
|
3573
|
+
deleteNodes(...nodes) {
|
|
3574
|
+
this.update.removeNodes.push(...nodes);
|
|
3575
|
+
return this;
|
|
3576
|
+
}
|
|
3564
3577
|
updateNode(node) {
|
|
3565
3578
|
this.update.updateNodes.push(node);
|
|
3566
3579
|
return this;
|
|
3567
3580
|
}
|
|
3581
|
+
updateNodes(...nodes) {
|
|
3582
|
+
this.update.updateNodes.push(...nodes);
|
|
3583
|
+
return this;
|
|
3584
|
+
}
|
|
3568
3585
|
addEdge(edge) {
|
|
3569
3586
|
this.update.addEdges.push(edge);
|
|
3570
3587
|
return this;
|
|
3571
3588
|
}
|
|
3589
|
+
addEdges(...edges) {
|
|
3590
|
+
this.update.addEdges.push(...edges);
|
|
3591
|
+
return this;
|
|
3592
|
+
}
|
|
3572
3593
|
deleteEdge(edge) {
|
|
3573
3594
|
this.update.removeEdges.push(edge);
|
|
3574
3595
|
return this;
|
|
3575
3596
|
}
|
|
3597
|
+
deleteEdges(...edges) {
|
|
3598
|
+
this.update.removeEdges.push(...edges);
|
|
3599
|
+
return this;
|
|
3600
|
+
}
|
|
3576
3601
|
updateEdge(edge) {
|
|
3577
3602
|
this.update.updateEdges.push(edge);
|
|
3578
3603
|
return this;
|
|
3579
3604
|
}
|
|
3605
|
+
updateEdges(...edges) {
|
|
3606
|
+
this.update.updateEdges.push(...edges);
|
|
3607
|
+
return this;
|
|
3608
|
+
}
|
|
3580
3609
|
static add(nodes, edges) {
|
|
3581
3610
|
const updater = new _Updater();
|
|
3582
3611
|
updater.update.addNodes = nodes;
|
|
@@ -3607,19 +3636,8 @@ var API = class {
|
|
|
3607
3636
|
constructor(args) {
|
|
3608
3637
|
this.root = args.root;
|
|
3609
3638
|
this.options = applyDefaults(args.options);
|
|
3610
|
-
let graph2 = new Graph({ options: this.options.graph });
|
|
3611
|
-
this.state = { graph: graph2, update: null };
|
|
3612
3639
|
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;
|
|
3640
|
+
this.reset();
|
|
3623
3641
|
this.canvas = new Canvas(this, {
|
|
3624
3642
|
...this.options.canvas,
|
|
3625
3643
|
dummyNodeSize: this.options.graph.dummyNodeSize,
|
|
@@ -3633,6 +3651,31 @@ var API = class {
|
|
|
3633
3651
|
this.history = [];
|
|
3634
3652
|
}
|
|
3635
3653
|
}
|
|
3654
|
+
reset() {
|
|
3655
|
+
let graph2 = new Graph({ options: this.options.graph });
|
|
3656
|
+
this.state = { graph: graph2, update: null };
|
|
3657
|
+
this.seq = [this.state];
|
|
3658
|
+
this.index = 0;
|
|
3659
|
+
this.nodeIds = /* @__PURE__ */ new Map();
|
|
3660
|
+
this.edgeIds = /* @__PURE__ */ new Map();
|
|
3661
|
+
this.nodeVersions = /* @__PURE__ */ new Map();
|
|
3662
|
+
this.nodeOverrides = /* @__PURE__ */ new Map();
|
|
3663
|
+
this.edgeOverrides = /* @__PURE__ */ new Map();
|
|
3664
|
+
this.nodeFields = /* @__PURE__ */ new Map();
|
|
3665
|
+
this.nextNodeId = 1;
|
|
3666
|
+
this.nextEdgeId = 1;
|
|
3667
|
+
}
|
|
3668
|
+
/** Initialize the API */
|
|
3669
|
+
async init() {
|
|
3670
|
+
const root = document.getElementById(this.root);
|
|
3671
|
+
if (!root) throw new Error("root element not found");
|
|
3672
|
+
root.appendChild(this.canvas.container);
|
|
3673
|
+
await this.applyHistory();
|
|
3674
|
+
}
|
|
3675
|
+
async applyHistory() {
|
|
3676
|
+
for (const update of this.history)
|
|
3677
|
+
await this.applyUpdate(update);
|
|
3678
|
+
}
|
|
3636
3679
|
/** Current history index (0-based) */
|
|
3637
3680
|
getHistoryIndex() {
|
|
3638
3681
|
return this.index;
|
|
@@ -3645,17 +3688,25 @@ var API = class {
|
|
|
3645
3688
|
setEditable(editable) {
|
|
3646
3689
|
this.canvas.editMode.editable = editable;
|
|
3647
3690
|
}
|
|
3691
|
+
/** Replace entire history (clears prior) */
|
|
3692
|
+
async replaceHistory(frames) {
|
|
3693
|
+
this.reset();
|
|
3694
|
+
this.history = frames;
|
|
3695
|
+
await this.applyHistory();
|
|
3696
|
+
}
|
|
3697
|
+
/** Rebuild from snapshot (nodes/edges) */
|
|
3698
|
+
async replaceSnapshot(nodes, edges, description) {
|
|
3699
|
+
this.reset();
|
|
3700
|
+
this.history = [{
|
|
3701
|
+
addNodes: nodes,
|
|
3702
|
+
addEdges: edges,
|
|
3703
|
+
description
|
|
3704
|
+
}];
|
|
3705
|
+
await this.applyHistory();
|
|
3706
|
+
}
|
|
3648
3707
|
get graph() {
|
|
3649
3708
|
return this.state.graph;
|
|
3650
3709
|
}
|
|
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
3710
|
/** Navigate to a different state */
|
|
3660
3711
|
nav(nav) {
|
|
3661
3712
|
let newIndex;
|
|
@@ -4077,6 +4128,989 @@ var API = class {
|
|
|
4077
4128
|
}
|
|
4078
4129
|
};
|
|
4079
4130
|
|
|
4131
|
+
// src/api/ingest.ts
|
|
4132
|
+
var Ingest = class {
|
|
4133
|
+
constructor(api) {
|
|
4134
|
+
this.api = api;
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Apply an incoming ingest message to the API.
|
|
4138
|
+
* - snapshot: rebuild state from nodes/edges (clears prior history)
|
|
4139
|
+
* - update: apply incremental update
|
|
4140
|
+
* - history: initialize from a set of frames (clears prior history)
|
|
4141
|
+
*/
|
|
4142
|
+
async apply(msg) {
|
|
4143
|
+
switch (msg.type) {
|
|
4144
|
+
case "snapshot": {
|
|
4145
|
+
await this.api.replaceSnapshot(msg.nodes, msg.edges, msg.description);
|
|
4146
|
+
break;
|
|
4147
|
+
}
|
|
4148
|
+
case "update": {
|
|
4149
|
+
await this.api.update((u) => {
|
|
4150
|
+
if (msg.addNodes) u.addNodes(...msg.addNodes);
|
|
4151
|
+
if (msg.removeNodes) u.deleteNodes(...msg.removeNodes);
|
|
4152
|
+
if (msg.updateNodes) u.updateNodes(...msg.updateNodes);
|
|
4153
|
+
if (msg.addEdges) u.addEdges(...msg.addEdges);
|
|
4154
|
+
if (msg.removeEdges) u.deleteEdges(...msg.removeEdges);
|
|
4155
|
+
if (msg.updateEdges) u.updateEdges(...msg.updateEdges);
|
|
4156
|
+
if (msg.description) u.describe(msg.description);
|
|
4157
|
+
});
|
|
4158
|
+
break;
|
|
4159
|
+
}
|
|
4160
|
+
case "history": {
|
|
4161
|
+
await this.api.replaceHistory(msg.frames);
|
|
4162
|
+
break;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
};
|
|
4167
|
+
|
|
4168
|
+
// src/api/sources/WebSocketSource.ts
|
|
4169
|
+
var WebSocketSource = class {
|
|
4170
|
+
url;
|
|
4171
|
+
ws = null;
|
|
4172
|
+
onMessage;
|
|
4173
|
+
onStatus;
|
|
4174
|
+
reconnectMs;
|
|
4175
|
+
closedByUser = false;
|
|
4176
|
+
connectStartTime = null;
|
|
4177
|
+
totalTimeoutMs = 1e4;
|
|
4178
|
+
totalTimeoutTimer = null;
|
|
4179
|
+
constructor(url, onMessage, onStatus, reconnectMs = 1500) {
|
|
4180
|
+
this.url = url;
|
|
4181
|
+
this.onMessage = onMessage;
|
|
4182
|
+
this.onStatus = onStatus;
|
|
4183
|
+
this.reconnectMs = reconnectMs;
|
|
4184
|
+
}
|
|
4185
|
+
connect() {
|
|
4186
|
+
this.closedByUser = false;
|
|
4187
|
+
this.connectStartTime = Date.now();
|
|
4188
|
+
this.startTotalTimeout();
|
|
4189
|
+
this.open();
|
|
4190
|
+
}
|
|
4191
|
+
disconnect() {
|
|
4192
|
+
this.closedByUser = true;
|
|
4193
|
+
this.clearTotalTimeout();
|
|
4194
|
+
if (this.ws) {
|
|
4195
|
+
try {
|
|
4196
|
+
this.ws.close();
|
|
4197
|
+
} catch {
|
|
4198
|
+
}
|
|
4199
|
+
this.ws = null;
|
|
4200
|
+
}
|
|
4201
|
+
this.onStatus?.("closed");
|
|
4202
|
+
}
|
|
4203
|
+
startTotalTimeout() {
|
|
4204
|
+
this.clearTotalTimeout();
|
|
4205
|
+
this.totalTimeoutTimer = window.setTimeout(() => {
|
|
4206
|
+
if (!this.closedByUser && this.ws?.readyState !== WebSocket.OPEN) {
|
|
4207
|
+
this.closedByUser = true;
|
|
4208
|
+
if (this.ws) {
|
|
4209
|
+
try {
|
|
4210
|
+
this.ws.close();
|
|
4211
|
+
} catch {
|
|
4212
|
+
}
|
|
4213
|
+
this.ws = null;
|
|
4214
|
+
}
|
|
4215
|
+
this.clearTotalTimeout();
|
|
4216
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4217
|
+
}
|
|
4218
|
+
}, this.totalTimeoutMs);
|
|
4219
|
+
}
|
|
4220
|
+
clearTotalTimeout() {
|
|
4221
|
+
if (this.totalTimeoutTimer !== null) {
|
|
4222
|
+
clearTimeout(this.totalTimeoutTimer);
|
|
4223
|
+
this.totalTimeoutTimer = null;
|
|
4224
|
+
}
|
|
4225
|
+
this.connectStartTime = null;
|
|
4226
|
+
}
|
|
4227
|
+
open() {
|
|
4228
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4229
|
+
if (!this.closedByUser) {
|
|
4230
|
+
this.closedByUser = true;
|
|
4231
|
+
this.clearTotalTimeout();
|
|
4232
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4233
|
+
}
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
this.onStatus?.(this.ws ? "reconnecting" : "connecting");
|
|
4237
|
+
const ws = new WebSocket(this.url);
|
|
4238
|
+
this.ws = ws;
|
|
4239
|
+
ws.onopen = () => {
|
|
4240
|
+
this.clearTotalTimeout();
|
|
4241
|
+
this.onStatus?.("connected");
|
|
4242
|
+
};
|
|
4243
|
+
ws.onerror = (e) => {
|
|
4244
|
+
this.onStatus?.("error", e);
|
|
4245
|
+
};
|
|
4246
|
+
ws.onclose = () => {
|
|
4247
|
+
if (this.closedByUser) {
|
|
4248
|
+
this.onStatus?.("closed");
|
|
4249
|
+
return;
|
|
4250
|
+
}
|
|
4251
|
+
if (this.connectStartTime && Date.now() - this.connectStartTime >= this.totalTimeoutMs) {
|
|
4252
|
+
this.closedByUser = true;
|
|
4253
|
+
this.clearTotalTimeout();
|
|
4254
|
+
this.onStatus?.("error", new Error("Connection timeout after 10 seconds"));
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
this.onStatus?.("reconnecting");
|
|
4258
|
+
setTimeout(() => this.open(), this.reconnectMs);
|
|
4259
|
+
};
|
|
4260
|
+
ws.onmessage = (ev) => {
|
|
4261
|
+
const data = typeof ev.data === "string" ? ev.data : "";
|
|
4262
|
+
const lines = data.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4263
|
+
for (const line of lines) {
|
|
4264
|
+
try {
|
|
4265
|
+
const obj = JSON.parse(line);
|
|
4266
|
+
this.onMessage(obj);
|
|
4267
|
+
} catch {
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
|
|
4274
|
+
// src/api/sources/FileSystemSource.ts
|
|
4275
|
+
var FileSystemSource = class {
|
|
4276
|
+
handle = null;
|
|
4277
|
+
onMessage;
|
|
4278
|
+
onStatus;
|
|
4279
|
+
timer = null;
|
|
4280
|
+
lastSize = 0;
|
|
4281
|
+
filename;
|
|
4282
|
+
intervalMs;
|
|
4283
|
+
constructor(onMessage, onStatus, filename = "graph.ndjson", intervalMs = 1e3) {
|
|
4284
|
+
this.onMessage = onMessage;
|
|
4285
|
+
this.onStatus = onStatus;
|
|
4286
|
+
this.filename = filename;
|
|
4287
|
+
this.intervalMs = intervalMs;
|
|
4288
|
+
}
|
|
4289
|
+
async openDirectory() {
|
|
4290
|
+
try {
|
|
4291
|
+
const dir = await window.showDirectoryPicker?.();
|
|
4292
|
+
if (!dir) throw new Error("File System Access not supported or cancelled");
|
|
4293
|
+
const handle = await dir.getFileHandle(this.filename, { create: false });
|
|
4294
|
+
this.handle = handle;
|
|
4295
|
+
this.onStatus?.("opened", { file: this.filename });
|
|
4296
|
+
this.lastSize = 0;
|
|
4297
|
+
this.startPolling();
|
|
4298
|
+
} catch (e) {
|
|
4299
|
+
this.onStatus?.("error", e);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
close() {
|
|
4303
|
+
if (this.timer) {
|
|
4304
|
+
window.clearInterval(this.timer);
|
|
4305
|
+
this.timer = null;
|
|
4306
|
+
}
|
|
4307
|
+
this.handle = null;
|
|
4308
|
+
this.onStatus?.("closed");
|
|
4309
|
+
}
|
|
4310
|
+
startPolling() {
|
|
4311
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4312
|
+
this.timer = window.setInterval(() => this.readNewLines(), this.intervalMs);
|
|
4313
|
+
}
|
|
4314
|
+
async readNewLines() {
|
|
4315
|
+
try {
|
|
4316
|
+
if (!this.handle) return;
|
|
4317
|
+
this.onStatus?.("reading");
|
|
4318
|
+
const file = await this.handle.getFile();
|
|
4319
|
+
if (file.size === this.lastSize) return;
|
|
4320
|
+
const slice = await file.slice(this.lastSize).text();
|
|
4321
|
+
this.lastSize = file.size;
|
|
4322
|
+
const lines = slice.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4323
|
+
for (const line of lines) {
|
|
4324
|
+
try {
|
|
4325
|
+
const obj = JSON.parse(line);
|
|
4326
|
+
this.onMessage(obj);
|
|
4327
|
+
} catch {
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
} catch (e) {
|
|
4331
|
+
this.onStatus?.("error", e);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
};
|
|
4335
|
+
|
|
4336
|
+
// src/api/sources/FileSource.ts
|
|
4337
|
+
var FileSource = class {
|
|
4338
|
+
url;
|
|
4339
|
+
onMessage;
|
|
4340
|
+
onStatus;
|
|
4341
|
+
timer = null;
|
|
4342
|
+
lastETag = null;
|
|
4343
|
+
lastContent = "";
|
|
4344
|
+
intervalMs = 1e3;
|
|
4345
|
+
closed = false;
|
|
4346
|
+
constructor(url, onMessage, onStatus, intervalMs = 1e3) {
|
|
4347
|
+
this.url = url;
|
|
4348
|
+
this.onMessage = onMessage;
|
|
4349
|
+
this.onStatus = onStatus;
|
|
4350
|
+
this.intervalMs = intervalMs;
|
|
4351
|
+
}
|
|
4352
|
+
async connect() {
|
|
4353
|
+
this.closed = false;
|
|
4354
|
+
this.lastETag = null;
|
|
4355
|
+
this.lastContent = "";
|
|
4356
|
+
this.onStatus?.("opened");
|
|
4357
|
+
this.startPolling();
|
|
4358
|
+
}
|
|
4359
|
+
close() {
|
|
4360
|
+
this.closed = true;
|
|
4361
|
+
if (this.timer) {
|
|
4362
|
+
window.clearInterval(this.timer);
|
|
4363
|
+
this.timer = null;
|
|
4364
|
+
}
|
|
4365
|
+
this.onStatus?.("closed");
|
|
4366
|
+
}
|
|
4367
|
+
startPolling() {
|
|
4368
|
+
if (this.timer) window.clearInterval(this.timer);
|
|
4369
|
+
this.timer = window.setInterval(() => this.poll(), this.intervalMs);
|
|
4370
|
+
this.poll();
|
|
4371
|
+
}
|
|
4372
|
+
async poll() {
|
|
4373
|
+
if (this.closed) return;
|
|
4374
|
+
try {
|
|
4375
|
+
this.onStatus?.("reading");
|
|
4376
|
+
const headers = {};
|
|
4377
|
+
if (this.lastETag) {
|
|
4378
|
+
headers["If-None-Match"] = this.lastETag;
|
|
4379
|
+
}
|
|
4380
|
+
const response = await fetch(this.url, { headers });
|
|
4381
|
+
if (response.status === 304) {
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
if (!response.ok) {
|
|
4385
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4386
|
+
}
|
|
4387
|
+
const etag = response.headers.get("ETag");
|
|
4388
|
+
if (etag) {
|
|
4389
|
+
this.lastETag = etag;
|
|
4390
|
+
}
|
|
4391
|
+
const content = await response.text();
|
|
4392
|
+
if (content === this.lastContent) {
|
|
4393
|
+
return;
|
|
4394
|
+
}
|
|
4395
|
+
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4396
|
+
const lastContentLines = this.lastContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4397
|
+
const newLines = lines.slice(lastContentLines.length);
|
|
4398
|
+
for (const line of newLines) {
|
|
4399
|
+
try {
|
|
4400
|
+
const obj = JSON.parse(line);
|
|
4401
|
+
this.onMessage(obj);
|
|
4402
|
+
} catch {
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
this.lastContent = content;
|
|
4406
|
+
} catch (e) {
|
|
4407
|
+
this.onStatus?.("error", e);
|
|
4408
|
+
}
|
|
4409
|
+
}
|
|
4410
|
+
};
|
|
4411
|
+
|
|
4412
|
+
// src/playground/playground.ts
|
|
4413
|
+
var import_styles2 = __toESM(require("./styles.css?raw"), 1);
|
|
4414
|
+
var Playground = class {
|
|
4415
|
+
options;
|
|
4416
|
+
rootElement;
|
|
4417
|
+
currentExample;
|
|
4418
|
+
currentGraph = null;
|
|
4419
|
+
ingest = null;
|
|
4420
|
+
isEditable = false;
|
|
4421
|
+
wsSource = null;
|
|
4422
|
+
fsSource = null;
|
|
4423
|
+
fileSource = null;
|
|
4424
|
+
wsStatus = "disconnected";
|
|
4425
|
+
fsStatus = "disconnected";
|
|
4426
|
+
fileStatus = "disconnected";
|
|
4427
|
+
activeSourceType = null;
|
|
4428
|
+
wsUrl = "ws://localhost:8787";
|
|
4429
|
+
sourceModal = null;
|
|
4430
|
+
helpOverlay = null;
|
|
4431
|
+
exampleList;
|
|
4432
|
+
graphContainerId;
|
|
4433
|
+
constructor(options) {
|
|
4434
|
+
this.options = options;
|
|
4435
|
+
this.exampleList = Object.keys(options.examples);
|
|
4436
|
+
this.currentExample = options.defaultExample || this.exampleList[0];
|
|
4437
|
+
this.graphContainerId = `playground-graph-${Math.random().toString(36).substr(2, 9)}`;
|
|
4438
|
+
if (typeof options.root === "string") {
|
|
4439
|
+
const el = document.getElementById(options.root);
|
|
4440
|
+
if (!el) throw new Error(`Element with id "${options.root}" not found`);
|
|
4441
|
+
this.rootElement = el;
|
|
4442
|
+
} else {
|
|
4443
|
+
this.rootElement = options.root;
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
async init() {
|
|
4447
|
+
this.injectStyles();
|
|
4448
|
+
this.createDOM();
|
|
4449
|
+
this.setupEventListeners();
|
|
4450
|
+
await this.renderGraph();
|
|
4451
|
+
this.updateSourceIcon();
|
|
4452
|
+
this.connectExampleSource();
|
|
4453
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4454
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4455
|
+
}
|
|
4456
|
+
injectStyles() {
|
|
4457
|
+
if (!document.getElementById("g3p-playground-styles")) {
|
|
4458
|
+
const styleEl = document.createElement("style");
|
|
4459
|
+
styleEl.id = "g3p-playground-styles";
|
|
4460
|
+
styleEl.textContent = import_styles2.default;
|
|
4461
|
+
document.head.appendChild(styleEl);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
createDOM() {
|
|
4465
|
+
const exampleList = this.exampleList.map((key, i) => {
|
|
4466
|
+
const example = this.options.examples[key];
|
|
4467
|
+
const isActive = i === 0 || key === this.currentExample;
|
|
4468
|
+
return `
|
|
4469
|
+
<button class="example-btn ${isActive ? "active" : ""}" data-example="${key}">
|
|
4470
|
+
${example.name}
|
|
4471
|
+
</button>
|
|
4472
|
+
`;
|
|
4473
|
+
}).join("");
|
|
4474
|
+
this.rootElement.innerHTML = `
|
|
4475
|
+
<main class="playground">
|
|
4476
|
+
<div class="sidebar">
|
|
4477
|
+
<h2>Examples</h2>
|
|
4478
|
+
<div class="example-list">
|
|
4479
|
+
${exampleList}
|
|
4480
|
+
</div>
|
|
4481
|
+
|
|
4482
|
+
<h2>Options</h2>
|
|
4483
|
+
<div class="options">
|
|
4484
|
+
<div class="option-group">
|
|
4485
|
+
<label>Orientation</label>
|
|
4486
|
+
<select id="orientation">
|
|
4487
|
+
<option value="TB">Top to Bottom</option>
|
|
4488
|
+
<option value="BT">Bottom to Top</option>
|
|
4489
|
+
<option value="LR">Left to Right</option>
|
|
4490
|
+
<option value="RL">Right to Left</option>
|
|
4491
|
+
</select>
|
|
4492
|
+
</div>
|
|
4493
|
+
|
|
4494
|
+
<div class="option-group">
|
|
4495
|
+
<label>Port Style</label>
|
|
4496
|
+
<select id="portStyle">
|
|
4497
|
+
<option value="outside">Outside</option>
|
|
4498
|
+
<option value="inside">Inside</option>
|
|
4499
|
+
</select>
|
|
4500
|
+
</div>
|
|
4501
|
+
|
|
4502
|
+
<div class="option-group">
|
|
4503
|
+
<label>
|
|
4504
|
+
<input type="checkbox" id="portLabelRotate" />
|
|
4505
|
+
Rotate Port Labels
|
|
4506
|
+
</label>
|
|
4507
|
+
</div>
|
|
4508
|
+
|
|
4509
|
+
<div class="option-group">
|
|
4510
|
+
<label>Theme</label>
|
|
4511
|
+
<select id="colorMode">
|
|
4512
|
+
<option value="system">System</option>
|
|
4513
|
+
<option value="light">Light</option>
|
|
4514
|
+
<option value="dark">Dark</option>
|
|
4515
|
+
</select>
|
|
4516
|
+
</div>
|
|
4517
|
+
</div>
|
|
4518
|
+
</div>
|
|
4519
|
+
|
|
4520
|
+
<div class="graph-area">
|
|
4521
|
+
<div class="graph-toolbar">
|
|
4522
|
+
<div class="nav-controls">
|
|
4523
|
+
<button class="nav-btn" id="nav-first" title="First (Home)">\u23EE</button>
|
|
4524
|
+
<button class="nav-btn" id="nav-prev" title="Previous (\u2190)">\u25C0</button>
|
|
4525
|
+
<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>
|
|
4526
|
+
<button class="nav-btn" id="nav-next" title="Next (\u2192)">\u25B6</button>
|
|
4527
|
+
<button class="nav-btn" id="nav-last" title="Last (End)">\u23ED</button>
|
|
4528
|
+
</div>
|
|
4529
|
+
<div class="connect-controls" style="display:flex; gap:.5rem; align-items:center;">
|
|
4530
|
+
<button class="nav-btn source-icon-btn" id="source-icon" title="Data Source Connection">\u{1F4E1}</button>
|
|
4531
|
+
</div>
|
|
4532
|
+
<button class="nav-btn" id="help-btn" title="How to edit">\u2753</button>
|
|
4533
|
+
<button class="nav-btn" id="edit-toggle" title="Toggle edit mode">\u270E Edit</button>
|
|
4534
|
+
<button class="nav-btn" id="rebuild" title="Rebuild graph from scratch">\u{1F504} Rebuild</button>
|
|
4535
|
+
</div>
|
|
4536
|
+
<div class="graph-container" id="${this.graphContainerId}"></div>
|
|
4537
|
+
</div>
|
|
4538
|
+
</main>
|
|
4539
|
+
`;
|
|
4540
|
+
}
|
|
4541
|
+
setupEventListeners() {
|
|
4542
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((btn) => {
|
|
4543
|
+
btn.addEventListener("click", () => {
|
|
4544
|
+
this.rootElement.querySelectorAll(".example-btn").forEach((b) => b.classList.remove("active"));
|
|
4545
|
+
btn.classList.add("active");
|
|
4546
|
+
this.currentExample = btn.getAttribute("data-example") || this.exampleList[0];
|
|
4547
|
+
this.renderGraph();
|
|
4548
|
+
this.connectExampleSource();
|
|
4549
|
+
});
|
|
4550
|
+
});
|
|
4551
|
+
this.rootElement.querySelectorAll(".options select, .options input").forEach((el) => {
|
|
4552
|
+
el.addEventListener("change", () => this.renderGraph());
|
|
4553
|
+
});
|
|
4554
|
+
this.rootElement.querySelector("#nav-first")?.addEventListener("click", () => {
|
|
4555
|
+
this.currentGraph?.nav("first");
|
|
4556
|
+
this.updateHistoryLabel();
|
|
4557
|
+
});
|
|
4558
|
+
this.rootElement.querySelector("#nav-prev")?.addEventListener("click", () => {
|
|
4559
|
+
this.currentGraph?.nav("prev");
|
|
4560
|
+
this.updateHistoryLabel();
|
|
4561
|
+
});
|
|
4562
|
+
this.rootElement.querySelector("#nav-next")?.addEventListener("click", () => {
|
|
4563
|
+
this.currentGraph?.nav("next");
|
|
4564
|
+
this.updateHistoryLabel();
|
|
4565
|
+
});
|
|
4566
|
+
this.rootElement.querySelector("#nav-last")?.addEventListener("click", () => {
|
|
4567
|
+
this.currentGraph?.nav("last");
|
|
4568
|
+
this.updateHistoryLabel();
|
|
4569
|
+
});
|
|
4570
|
+
this.rootElement.querySelector("#rebuild")?.addEventListener("click", () => {
|
|
4571
|
+
this.currentGraph?.rebuild();
|
|
4572
|
+
});
|
|
4573
|
+
this.rootElement.querySelector("#edit-toggle")?.addEventListener("click", () => {
|
|
4574
|
+
this.isEditable = !this.isEditable;
|
|
4575
|
+
const btn = this.rootElement.querySelector("#edit-toggle");
|
|
4576
|
+
if (btn) btn.textContent = this.isEditable ? "\u2713 Done" : "\u270E Edit";
|
|
4577
|
+
const rebuildBtn = this.rootElement.querySelector("#rebuild");
|
|
4578
|
+
if (rebuildBtn) rebuildBtn.disabled = !this.isEditable;
|
|
4579
|
+
try {
|
|
4580
|
+
this.currentGraph?.setEditable?.(this.isEditable);
|
|
4581
|
+
} catch {
|
|
4582
|
+
}
|
|
4583
|
+
});
|
|
4584
|
+
this.rootElement.querySelector("#help-btn")?.addEventListener("click", () => this.openHelp());
|
|
4585
|
+
const sourceIconBtn = this.rootElement.querySelector("#source-icon");
|
|
4586
|
+
if (sourceIconBtn) {
|
|
4587
|
+
sourceIconBtn.addEventListener("click", (e) => {
|
|
4588
|
+
e.preventDefault();
|
|
4589
|
+
e.stopPropagation();
|
|
4590
|
+
this.openSourceModal();
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
document.addEventListener("keydown", (e) => {
|
|
4594
|
+
if (!this.currentGraph) return;
|
|
4595
|
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
|
|
4596
|
+
switch (e.key) {
|
|
4597
|
+
case "Home":
|
|
4598
|
+
this.currentGraph.nav("first");
|
|
4599
|
+
this.updateHistoryLabel();
|
|
4600
|
+
break;
|
|
4601
|
+
case "End":
|
|
4602
|
+
this.currentGraph.nav("last");
|
|
4603
|
+
this.updateHistoryLabel();
|
|
4604
|
+
break;
|
|
4605
|
+
case "ArrowLeft":
|
|
4606
|
+
this.currentGraph.nav("prev");
|
|
4607
|
+
this.updateHistoryLabel();
|
|
4608
|
+
break;
|
|
4609
|
+
case "ArrowRight":
|
|
4610
|
+
this.currentGraph.nav("next");
|
|
4611
|
+
this.updateHistoryLabel();
|
|
4612
|
+
break;
|
|
4613
|
+
}
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4616
|
+
getResolvedColorMode() {
|
|
4617
|
+
const mode = this.rootElement.querySelector("#colorMode")?.value;
|
|
4618
|
+
if (mode === "system") {
|
|
4619
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4620
|
+
}
|
|
4621
|
+
return mode;
|
|
4622
|
+
}
|
|
4623
|
+
getOptions(exampleOptions) {
|
|
4624
|
+
const orientation = this.rootElement.querySelector("#orientation")?.value;
|
|
4625
|
+
return {
|
|
4626
|
+
graph: { orientation },
|
|
4627
|
+
canvas: {
|
|
4628
|
+
width: "100%",
|
|
4629
|
+
height: "100%",
|
|
4630
|
+
colorMode: this.getResolvedColorMode(),
|
|
4631
|
+
editable: this.isEditable,
|
|
4632
|
+
...exampleOptions?.canvas
|
|
4633
|
+
}
|
|
4634
|
+
};
|
|
4635
|
+
}
|
|
4636
|
+
async renderGraph() {
|
|
4637
|
+
const container = this.rootElement.querySelector(`#${this.graphContainerId}`);
|
|
4638
|
+
if (!container) return;
|
|
4639
|
+
container.innerHTML = "";
|
|
4640
|
+
const example = this.options.examples[this.currentExample];
|
|
4641
|
+
const options = this.getOptions(example.options);
|
|
4642
|
+
try {
|
|
4643
|
+
this.currentGraph = await graph({
|
|
4644
|
+
root: this.graphContainerId,
|
|
4645
|
+
nodes: example.nodes,
|
|
4646
|
+
edges: example.edges,
|
|
4647
|
+
options,
|
|
4648
|
+
events: {
|
|
4649
|
+
historyChange: () => this.updateHistoryLabel()
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4652
|
+
this.ingest = new Ingest(this.currentGraph);
|
|
4653
|
+
this.updateHistoryLabel();
|
|
4654
|
+
} catch (e) {
|
|
4655
|
+
console.error("Failed to render graph:", e);
|
|
4656
|
+
container.innerHTML = '<p style="padding: 2rem; color: #ef4444;">Failed to load graph</p>';
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
updateHistoryLabel() {
|
|
4660
|
+
const label = this.rootElement.querySelector("#history-label");
|
|
4661
|
+
if (!label || !this.currentGraph) return;
|
|
4662
|
+
try {
|
|
4663
|
+
const idx = this.currentGraph.getHistoryIndex?.() ?? 0;
|
|
4664
|
+
const len = this.currentGraph.getHistoryLength?.() ?? 1;
|
|
4665
|
+
label.textContent = `${idx + 1} / ${len}`;
|
|
4666
|
+
} catch {
|
|
4667
|
+
label.textContent = "\u2014 / \u2014";
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
connectExampleSource() {
|
|
4671
|
+
const example = this.options.examples[this.currentExample];
|
|
4672
|
+
if (!example.source) {
|
|
4673
|
+
this.disconnectAllSources();
|
|
4674
|
+
return;
|
|
4675
|
+
}
|
|
4676
|
+
this.disconnectAllSources();
|
|
4677
|
+
if (example.source.type === "websocket") {
|
|
4678
|
+
this.wsUrl = example.source.url;
|
|
4679
|
+
this.wsSource = new WebSocketSource(example.source.url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
4680
|
+
this.wsSource.connect();
|
|
4681
|
+
} else if (example.source.type === "file") {
|
|
4682
|
+
this.fileSource = new FileSource(example.source.path, this.handleIngestMessage.bind(this), this.updateFileStatus);
|
|
4683
|
+
this.fileSource.connect();
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
disconnectAllSources() {
|
|
4687
|
+
this.wsSource?.disconnect();
|
|
4688
|
+
this.fsSource?.close();
|
|
4689
|
+
this.fileSource?.close();
|
|
4690
|
+
this.wsSource = null;
|
|
4691
|
+
this.fsSource = null;
|
|
4692
|
+
this.fileSource = null;
|
|
4693
|
+
this.activeSourceType = null;
|
|
4694
|
+
this.wsStatus = "disconnected";
|
|
4695
|
+
this.fsStatus = "disconnected";
|
|
4696
|
+
this.fileStatus = "disconnected";
|
|
4697
|
+
this.updateSourceIcon();
|
|
4698
|
+
}
|
|
4699
|
+
openHelp() {
|
|
4700
|
+
if (!this.helpOverlay) {
|
|
4701
|
+
this.helpOverlay = document.createElement("div");
|
|
4702
|
+
this.helpOverlay.className = "modal-overlay";
|
|
4703
|
+
this.helpOverlay.innerHTML = `
|
|
4704
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="help-title">
|
|
4705
|
+
<div class="modal-header">
|
|
4706
|
+
<h3 id="help-title">Editing the Graph</h3>
|
|
4707
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4708
|
+
</div>
|
|
4709
|
+
<div class="modal-body">
|
|
4710
|
+
<p>Here's how to edit the graph:</p>
|
|
4711
|
+
<ul>
|
|
4712
|
+
<li><strong>Enable editing</strong>: Click "Edit"</li>
|
|
4713
|
+
<li><strong>Add a node</strong>: Double\u2011click an empty area</li>
|
|
4714
|
+
<li><strong>Edit a node</strong>: Double\u2011click a node</li>
|
|
4715
|
+
<li><strong>Edit an edge</strong>: Double\u2011click an edge</li>
|
|
4716
|
+
<li><strong>Create an edge</strong>: Click and drag from a node (or its port) onto another node; press Esc to cancel</li>
|
|
4717
|
+
<li><strong>Pan</strong>: Drag on canvas or edges; <strong>Zoom</strong>: Mouse wheel or controls</li>
|
|
4718
|
+
<li><strong>Rebuild</strong>: Use "Rebuild" to re-layout from scratch (enabled in edit mode)</li>
|
|
4719
|
+
</ul>
|
|
4720
|
+
<p>When you're done, click "Done" to lock the canvas.</p>
|
|
4721
|
+
</div>
|
|
4722
|
+
</div>
|
|
4723
|
+
`;
|
|
4724
|
+
document.body.appendChild(this.helpOverlay);
|
|
4725
|
+
this.helpOverlay.addEventListener("click", (e) => {
|
|
4726
|
+
const target = e.target;
|
|
4727
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4728
|
+
this.closeHelp();
|
|
4729
|
+
}
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
this.helpOverlay.style.display = "flex";
|
|
4733
|
+
}
|
|
4734
|
+
closeHelp() {
|
|
4735
|
+
if (this.helpOverlay) this.helpOverlay.style.display = "none";
|
|
4736
|
+
}
|
|
4737
|
+
handleIngestMessage = async (msg) => {
|
|
4738
|
+
if (!this.ingest) return;
|
|
4739
|
+
await this.ingest.apply(msg);
|
|
4740
|
+
};
|
|
4741
|
+
updateSourceIcon() {
|
|
4742
|
+
const iconBtn = this.rootElement.querySelector("#source-icon");
|
|
4743
|
+
if (!iconBtn) return;
|
|
4744
|
+
iconBtn.classList.remove("active", "connecting", "error");
|
|
4745
|
+
const isConnected = this.activeSourceType === "ws" && this.wsStatus === "connected" || this.activeSourceType === "folder" && this.fsStatus === "connected" || this.activeSourceType === "file" && this.fileStatus === "connected";
|
|
4746
|
+
const isConnecting = this.activeSourceType === "ws" && this.wsStatus === "connecting" || this.activeSourceType === "folder" && this.fsStatus === "opening" || this.activeSourceType === "file" && this.fileStatus === "connecting";
|
|
4747
|
+
const hasError = this.activeSourceType === "ws" && this.wsStatus === "error" || this.activeSourceType === "folder" && this.fsStatus === "error" || this.activeSourceType === "file" && this.fileStatus === "error";
|
|
4748
|
+
let icon = "\u{1F4E1}";
|
|
4749
|
+
if (this.activeSourceType === "folder") {
|
|
4750
|
+
icon = "\u{1F4C1}";
|
|
4751
|
+
} else if (this.activeSourceType === "file") {
|
|
4752
|
+
icon = "\u{1F4C4}";
|
|
4753
|
+
}
|
|
4754
|
+
if (isConnected) {
|
|
4755
|
+
iconBtn.classList.add("active");
|
|
4756
|
+
iconBtn.textContent = icon;
|
|
4757
|
+
} else if (isConnecting) {
|
|
4758
|
+
iconBtn.classList.add("connecting");
|
|
4759
|
+
iconBtn.textContent = icon;
|
|
4760
|
+
} else if (hasError) {
|
|
4761
|
+
iconBtn.classList.add("error");
|
|
4762
|
+
iconBtn.textContent = icon;
|
|
4763
|
+
} else {
|
|
4764
|
+
iconBtn.textContent = "\u{1F4E1}";
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
updateWsStatus = (status, detail) => {
|
|
4768
|
+
if (status === "connecting" || status === "reconnecting") {
|
|
4769
|
+
this.wsStatus = "connecting";
|
|
4770
|
+
this.activeSourceType = "ws";
|
|
4771
|
+
} else if (status === "connected") {
|
|
4772
|
+
this.wsStatus = "connected";
|
|
4773
|
+
this.activeSourceType = "ws";
|
|
4774
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4775
|
+
this.fsSource.close();
|
|
4776
|
+
}
|
|
4777
|
+
if (this.fileSource && this.fileStatus === "connected") {
|
|
4778
|
+
this.fileSource.close();
|
|
4779
|
+
}
|
|
4780
|
+
} else if (status === "error") {
|
|
4781
|
+
this.wsStatus = "error";
|
|
4782
|
+
} else {
|
|
4783
|
+
this.wsStatus = "disconnected";
|
|
4784
|
+
if (this.activeSourceType === "ws") {
|
|
4785
|
+
this.activeSourceType = null;
|
|
4786
|
+
}
|
|
4787
|
+
}
|
|
4788
|
+
this.updateSourceIcon();
|
|
4789
|
+
this.updateSourceModal();
|
|
4790
|
+
};
|
|
4791
|
+
updateFsStatus = (status, detail) => {
|
|
4792
|
+
if (status === "opened") {
|
|
4793
|
+
this.fsStatus = "opening";
|
|
4794
|
+
this.activeSourceType = "folder";
|
|
4795
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4796
|
+
this.wsSource.disconnect();
|
|
4797
|
+
}
|
|
4798
|
+
} else if (status === "reading") {
|
|
4799
|
+
this.fsStatus = "connected";
|
|
4800
|
+
this.activeSourceType = "folder";
|
|
4801
|
+
} else if (status === "error") {
|
|
4802
|
+
this.fsStatus = "error";
|
|
4803
|
+
} else if (status === "closed") {
|
|
4804
|
+
this.fsStatus = "disconnected";
|
|
4805
|
+
if (this.activeSourceType === "folder") {
|
|
4806
|
+
this.activeSourceType = null;
|
|
4807
|
+
}
|
|
4808
|
+
} else {
|
|
4809
|
+
this.fsStatus = "disconnected";
|
|
4810
|
+
}
|
|
4811
|
+
this.updateSourceIcon();
|
|
4812
|
+
this.updateSourceModal();
|
|
4813
|
+
};
|
|
4814
|
+
updateFileStatus = (status, detail) => {
|
|
4815
|
+
if (status === "opened") {
|
|
4816
|
+
this.fileStatus = "connecting";
|
|
4817
|
+
this.activeSourceType = "file";
|
|
4818
|
+
if (this.wsSource && this.wsStatus === "connected") {
|
|
4819
|
+
this.wsSource.disconnect();
|
|
4820
|
+
}
|
|
4821
|
+
if (this.fsSource && this.fsStatus === "connected") {
|
|
4822
|
+
this.fsSource.close();
|
|
4823
|
+
}
|
|
4824
|
+
} else if (status === "reading") {
|
|
4825
|
+
this.fileStatus = "connected";
|
|
4826
|
+
this.activeSourceType = "file";
|
|
4827
|
+
} else if (status === "error") {
|
|
4828
|
+
this.fileStatus = "error";
|
|
4829
|
+
} else if (status === "closed") {
|
|
4830
|
+
this.fileStatus = "disconnected";
|
|
4831
|
+
if (this.activeSourceType === "file") {
|
|
4832
|
+
this.activeSourceType = null;
|
|
4833
|
+
}
|
|
4834
|
+
} else {
|
|
4835
|
+
this.fileStatus = "disconnected";
|
|
4836
|
+
}
|
|
4837
|
+
this.updateSourceIcon();
|
|
4838
|
+
this.updateSourceModal();
|
|
4839
|
+
};
|
|
4840
|
+
createSourceModal() {
|
|
4841
|
+
if (this.sourceModal) return this.sourceModal;
|
|
4842
|
+
this.sourceModal = document.createElement("div");
|
|
4843
|
+
this.sourceModal.className = "modal-overlay";
|
|
4844
|
+
this.sourceModal.style.display = "none";
|
|
4845
|
+
this.sourceModal.innerHTML = `
|
|
4846
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="source-modal-title">
|
|
4847
|
+
<div class="modal-header">
|
|
4848
|
+
<h3 id="source-modal-title">Data Source Connection</h3>
|
|
4849
|
+
<button class="modal-close" title="Close" aria-label="Close">\xD7</button>
|
|
4850
|
+
</div>
|
|
4851
|
+
<div class="modal-body">
|
|
4852
|
+
<div class="source-type-selector">
|
|
4853
|
+
<button class="source-type-option" data-source="ws">\u{1F4E1} WebSocket</button>
|
|
4854
|
+
<button class="source-type-option" data-source="folder">\u{1F4C1} Folder</button>
|
|
4855
|
+
</div>
|
|
4856
|
+
|
|
4857
|
+
<div class="source-controls" data-source="ws">
|
|
4858
|
+
<div class="form-group">
|
|
4859
|
+
<label for="source-modal-url">WebSocket URL</label>
|
|
4860
|
+
<input type="text" id="source-modal-url" value="${this.wsUrl}" />
|
|
4861
|
+
</div>
|
|
4862
|
+
<div class="button-group">
|
|
4863
|
+
<button id="source-modal-connect-ws" class="primary">Connect</button>
|
|
4864
|
+
<button id="source-modal-disconnect-ws">Disconnect</button>
|
|
4865
|
+
<button id="source-modal-change-ws">Change Connection</button>
|
|
4866
|
+
</div>
|
|
4867
|
+
</div>
|
|
4868
|
+
|
|
4869
|
+
<div class="source-controls" data-source="folder">
|
|
4870
|
+
<div class="form-group">
|
|
4871
|
+
<label>File System Source</label>
|
|
4872
|
+
<p style="font-size: 0.875rem; color: var(--color-text-muted); margin-top: 0.25rem;">
|
|
4873
|
+
Select a directory containing a graph.ndjson file to watch for changes.
|
|
4874
|
+
</p>
|
|
4875
|
+
</div>
|
|
4876
|
+
<div class="button-group">
|
|
4877
|
+
<button id="source-modal-connect-folder" class="primary">Open Folder</button>
|
|
4878
|
+
<button id="source-modal-disconnect-folder">Disconnect</button>
|
|
4879
|
+
</div>
|
|
4880
|
+
</div>
|
|
4881
|
+
|
|
4882
|
+
<div id="source-modal-status"></div>
|
|
4883
|
+
</div>
|
|
4884
|
+
</div>
|
|
4885
|
+
`;
|
|
4886
|
+
document.body.appendChild(this.sourceModal);
|
|
4887
|
+
this.sourceModal.addEventListener("click", (e) => {
|
|
4888
|
+
const target = e.target;
|
|
4889
|
+
if (target.classList.contains("modal-overlay") || target.classList.contains("modal-close")) {
|
|
4890
|
+
this.closeSourceModal();
|
|
4891
|
+
}
|
|
4892
|
+
});
|
|
4893
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4894
|
+
btn.addEventListener("click", () => {
|
|
4895
|
+
const sourceType = btn.getAttribute("data-source");
|
|
4896
|
+
if (sourceType) {
|
|
4897
|
+
this.selectSourceType(sourceType);
|
|
4898
|
+
}
|
|
4899
|
+
});
|
|
4900
|
+
});
|
|
4901
|
+
document.getElementById("source-modal-connect-ws")?.addEventListener("click", () => this.handleConnect());
|
|
4902
|
+
document.getElementById("source-modal-disconnect-ws")?.addEventListener("click", () => this.handleDisconnect());
|
|
4903
|
+
document.getElementById("source-modal-change-ws")?.addEventListener("click", () => this.handleChangeConnection());
|
|
4904
|
+
document.getElementById("source-modal-connect-folder")?.addEventListener("click", () => this.handleOpenFolder());
|
|
4905
|
+
document.getElementById("source-modal-disconnect-folder")?.addEventListener("click", () => this.handleCloseFolder());
|
|
4906
|
+
document.getElementById("source-modal-url")?.addEventListener("keydown", (e) => {
|
|
4907
|
+
if (e.key === "Enter" && this.wsStatus !== "connected") {
|
|
4908
|
+
this.handleConnect();
|
|
4909
|
+
}
|
|
4910
|
+
});
|
|
4911
|
+
return this.sourceModal;
|
|
4912
|
+
}
|
|
4913
|
+
selectSourceType(type, skipUpdate = false) {
|
|
4914
|
+
if (!this.sourceModal) return;
|
|
4915
|
+
this.sourceModal.querySelectorAll(".source-type-option").forEach((btn) => {
|
|
4916
|
+
if (btn.getAttribute("data-source") === type) {
|
|
4917
|
+
btn.classList.add("active");
|
|
4918
|
+
} else {
|
|
4919
|
+
btn.classList.remove("active");
|
|
4920
|
+
}
|
|
4921
|
+
});
|
|
4922
|
+
this.sourceModal.querySelectorAll(".source-controls").forEach((controls) => {
|
|
4923
|
+
if (controls.getAttribute("data-source") === type) {
|
|
4924
|
+
controls.classList.add("active");
|
|
4925
|
+
} else {
|
|
4926
|
+
controls.classList.remove("active");
|
|
4927
|
+
}
|
|
4928
|
+
});
|
|
4929
|
+
if (!skipUpdate) {
|
|
4930
|
+
this.updateSourceModalContent();
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4933
|
+
updateSourceModalContent() {
|
|
4934
|
+
if (!this.sourceModal) return;
|
|
4935
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
4936
|
+
const connectWsBtn = document.getElementById("source-modal-connect-ws");
|
|
4937
|
+
const disconnectWsBtn = document.getElementById("source-modal-disconnect-ws");
|
|
4938
|
+
const changeWsBtn = document.getElementById("source-modal-change-ws");
|
|
4939
|
+
if (urlInput && connectWsBtn && disconnectWsBtn && changeWsBtn) {
|
|
4940
|
+
const isWsConnected = this.wsStatus === "connected";
|
|
4941
|
+
const isWsConnecting = this.wsStatus === "connecting";
|
|
4942
|
+
connectWsBtn.disabled = isWsConnected || isWsConnecting;
|
|
4943
|
+
disconnectWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4944
|
+
changeWsBtn.disabled = !isWsConnected || isWsConnecting;
|
|
4945
|
+
urlInput.disabled = isWsConnecting;
|
|
4946
|
+
}
|
|
4947
|
+
const connectFolderBtn = document.getElementById("source-modal-connect-folder");
|
|
4948
|
+
const disconnectFolderBtn = document.getElementById("source-modal-disconnect-folder");
|
|
4949
|
+
if (connectFolderBtn && disconnectFolderBtn) {
|
|
4950
|
+
const isFolderConnected = this.fsStatus === "connected";
|
|
4951
|
+
const isFolderOpening = this.fsStatus === "opening";
|
|
4952
|
+
connectFolderBtn.disabled = isFolderConnected || isFolderOpening;
|
|
4953
|
+
disconnectFolderBtn.disabled = !isFolderConnected || isFolderOpening;
|
|
4954
|
+
}
|
|
4955
|
+
const statusDiv = document.getElementById("source-modal-status");
|
|
4956
|
+
if (!statusDiv) return;
|
|
4957
|
+
const currentUrl = urlInput?.value || this.wsUrl;
|
|
4958
|
+
statusDiv.innerHTML = "";
|
|
4959
|
+
if (this.activeSourceType === "ws") {
|
|
4960
|
+
if (this.wsStatus === "connecting") {
|
|
4961
|
+
statusDiv.innerHTML = `
|
|
4962
|
+
<div class="status-message info">
|
|
4963
|
+
<span class="loading-spinner"></span>
|
|
4964
|
+
Connecting to ${currentUrl}...
|
|
4965
|
+
</div>
|
|
4966
|
+
`;
|
|
4967
|
+
} else if (this.wsStatus === "connected") {
|
|
4968
|
+
statusDiv.innerHTML = `
|
|
4969
|
+
<div class="status-message success">
|
|
4970
|
+
\u2713 Connected to ${currentUrl}
|
|
4971
|
+
</div>
|
|
4972
|
+
`;
|
|
4973
|
+
} else if (this.wsStatus === "error") {
|
|
4974
|
+
statusDiv.innerHTML = `
|
|
4975
|
+
<div class="status-message error">
|
|
4976
|
+
\u2717 Connection error. Please check the URL and try again.
|
|
4977
|
+
</div>
|
|
4978
|
+
`;
|
|
4979
|
+
} else {
|
|
4980
|
+
statusDiv.innerHTML = `
|
|
4981
|
+
<div class="status-message info">
|
|
4982
|
+
Not connected
|
|
4983
|
+
</div>
|
|
4984
|
+
`;
|
|
4985
|
+
}
|
|
4986
|
+
} else if (this.activeSourceType === "folder") {
|
|
4987
|
+
if (this.fsStatus === "opening") {
|
|
4988
|
+
statusDiv.innerHTML = `
|
|
4989
|
+
<div class="status-message info">
|
|
4990
|
+
<span class="loading-spinner"></span>
|
|
4991
|
+
Opening folder...
|
|
4992
|
+
</div>
|
|
4993
|
+
`;
|
|
4994
|
+
} else if (this.fsStatus === "connected") {
|
|
4995
|
+
statusDiv.innerHTML = `
|
|
4996
|
+
<div class="status-message success">
|
|
4997
|
+
\u2713 Folder connected and watching for changes
|
|
4998
|
+
</div>
|
|
4999
|
+
`;
|
|
5000
|
+
} else if (this.fsStatus === "error") {
|
|
5001
|
+
statusDiv.innerHTML = `
|
|
5002
|
+
<div class="status-message error">
|
|
5003
|
+
\u2717 Error opening folder. Please try again.
|
|
5004
|
+
</div>
|
|
5005
|
+
`;
|
|
5006
|
+
} else {
|
|
5007
|
+
statusDiv.innerHTML = `
|
|
5008
|
+
<div class="status-message info">
|
|
5009
|
+
Not connected
|
|
5010
|
+
</div>
|
|
5011
|
+
`;
|
|
5012
|
+
}
|
|
5013
|
+
} else if (this.activeSourceType === "file") {
|
|
5014
|
+
const example = this.options.examples[this.currentExample];
|
|
5015
|
+
const filePath = example.source?.type === "file" ? example.source.path : "";
|
|
5016
|
+
if (this.fileStatus === "connecting") {
|
|
5017
|
+
statusDiv.innerHTML = `
|
|
5018
|
+
<div class="status-message info">
|
|
5019
|
+
<span class="loading-spinner"></span>
|
|
5020
|
+
Connecting to ${filePath}...
|
|
5021
|
+
</div>
|
|
5022
|
+
`;
|
|
5023
|
+
} else if (this.fileStatus === "connected") {
|
|
5024
|
+
statusDiv.innerHTML = `
|
|
5025
|
+
<div class="status-message success">
|
|
5026
|
+
\u2713 Connected to ${filePath}
|
|
5027
|
+
</div>
|
|
5028
|
+
`;
|
|
5029
|
+
} else if (this.fileStatus === "error") {
|
|
5030
|
+
statusDiv.innerHTML = `
|
|
5031
|
+
<div class="status-message error">
|
|
5032
|
+
\u2717 Error loading file. Please check the path and try again.
|
|
5033
|
+
</div>
|
|
5034
|
+
`;
|
|
5035
|
+
} else {
|
|
5036
|
+
statusDiv.innerHTML = `
|
|
5037
|
+
<div class="status-message info">
|
|
5038
|
+
Not connected
|
|
5039
|
+
</div>
|
|
5040
|
+
`;
|
|
5041
|
+
}
|
|
5042
|
+
} else {
|
|
5043
|
+
statusDiv.innerHTML = `
|
|
5044
|
+
<div class="status-message info">
|
|
5045
|
+
Select a source type to connect
|
|
5046
|
+
</div>
|
|
5047
|
+
`;
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
updateSourceModal() {
|
|
5051
|
+
if (!this.sourceModal) return;
|
|
5052
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5053
|
+
this.selectSourceType(activeType, true);
|
|
5054
|
+
this.updateSourceModalContent();
|
|
5055
|
+
}
|
|
5056
|
+
openSourceModal() {
|
|
5057
|
+
this.createSourceModal();
|
|
5058
|
+
if (this.sourceModal) {
|
|
5059
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5060
|
+
if (urlInput) {
|
|
5061
|
+
urlInput.value = this.wsUrl;
|
|
5062
|
+
}
|
|
5063
|
+
const activeType = (this.activeSourceType === "file" ? "ws" : this.activeSourceType) || "ws";
|
|
5064
|
+
this.selectSourceType(activeType);
|
|
5065
|
+
this.updateSourceModal();
|
|
5066
|
+
this.sourceModal.style.display = "flex";
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
closeSourceModal() {
|
|
5070
|
+
if (this.sourceModal) {
|
|
5071
|
+
this.sourceModal.style.display = "none";
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
handleConnect() {
|
|
5075
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5076
|
+
if (!urlInput) return;
|
|
5077
|
+
const url = urlInput.value.trim() || "ws://localhost:8787";
|
|
5078
|
+
this.wsUrl = url;
|
|
5079
|
+
if (this.wsSource) {
|
|
5080
|
+
this.wsSource.disconnect();
|
|
5081
|
+
}
|
|
5082
|
+
this.wsSource = new WebSocketSource(url, this.handleIngestMessage.bind(this), this.updateWsStatus);
|
|
5083
|
+
this.wsSource.connect();
|
|
5084
|
+
this.updateSourceModal();
|
|
5085
|
+
}
|
|
5086
|
+
handleDisconnect() {
|
|
5087
|
+
this.wsSource?.disconnect();
|
|
5088
|
+
this.updateSourceModal();
|
|
5089
|
+
}
|
|
5090
|
+
handleChangeConnection() {
|
|
5091
|
+
if (this.wsSource) {
|
|
5092
|
+
this.wsSource.disconnect();
|
|
5093
|
+
}
|
|
5094
|
+
const urlInput = document.getElementById("source-modal-url");
|
|
5095
|
+
if (urlInput) {
|
|
5096
|
+
urlInput.focus();
|
|
5097
|
+
urlInput.select();
|
|
5098
|
+
}
|
|
5099
|
+
this.updateSourceModal();
|
|
5100
|
+
}
|
|
5101
|
+
async handleOpenFolder() {
|
|
5102
|
+
if (!this.fsSource) {
|
|
5103
|
+
this.fsSource = new FileSystemSource(this.handleIngestMessage.bind(this), this.updateFsStatus);
|
|
5104
|
+
}
|
|
5105
|
+
this.updateSourceModal();
|
|
5106
|
+
await this.fsSource.openDirectory();
|
|
5107
|
+
}
|
|
5108
|
+
handleCloseFolder() {
|
|
5109
|
+
this.fsSource?.close();
|
|
5110
|
+
this.updateSourceModal();
|
|
5111
|
+
}
|
|
5112
|
+
};
|
|
5113
|
+
|
|
4080
5114
|
// src/index.ts
|
|
4081
5115
|
async function graph(args = { root: "app" }) {
|
|
4082
5116
|
const api = new API(args);
|
|
@@ -4086,5 +5120,10 @@ async function graph(args = { root: "app" }) {
|
|
|
4086
5120
|
var index_default = graph;
|
|
4087
5121
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4088
5122
|
0 && (module.exports = {
|
|
5123
|
+
FileSource,
|
|
5124
|
+
FileSystemSource,
|
|
5125
|
+
Ingest,
|
|
5126
|
+
Playground,
|
|
5127
|
+
WebSocketSource,
|
|
4089
5128
|
graph
|
|
4090
5129
|
});
|