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