backpack-viewer 0.5.1 → 0.7.0
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/bin/serve.js +159 -396
- package/dist/app/assets/index-D-H7agBH.js +12 -0
- package/dist/app/assets/index-DE73ngo-.css +1 -0
- package/dist/app/assets/index-DFW3OKgJ.js +6 -0
- package/dist/app/assets/layout-worker-4xak23M6.js +1 -0
- package/dist/app/index.html +2 -2
- package/dist/bridge.d.ts +22 -0
- package/dist/bridge.js +41 -0
- package/dist/canvas.d.ts +15 -0
- package/dist/canvas.js +352 -12
- package/dist/config.js +10 -0
- package/dist/copy-prompt.d.ts +17 -0
- package/dist/copy-prompt.js +81 -0
- package/dist/default-config.json +6 -1
- package/dist/dom-utils.d.ts +46 -0
- package/dist/dom-utils.js +57 -0
- package/dist/empty-state.js +63 -31
- package/dist/extensions/api.d.ts +15 -0
- package/dist/extensions/api.js +185 -0
- package/dist/extensions/chat/backpack-extension.json +23 -0
- package/dist/extensions/chat/src/index.js +32 -0
- package/dist/extensions/chat/src/panel.js +306 -0
- package/dist/extensions/chat/src/providers/anthropic.js +158 -0
- package/dist/extensions/chat/src/providers/types.js +15 -0
- package/dist/extensions/chat/src/tools.js +281 -0
- package/dist/extensions/chat/style.css +147 -0
- package/dist/extensions/event-bus.d.ts +12 -0
- package/dist/extensions/event-bus.js +30 -0
- package/dist/extensions/loader.d.ts +32 -0
- package/dist/extensions/loader.js +71 -0
- package/dist/extensions/manifest.d.ts +54 -0
- package/dist/extensions/manifest.js +116 -0
- package/dist/extensions/panel-mount.d.ts +26 -0
- package/dist/extensions/panel-mount.js +377 -0
- package/dist/extensions/taskbar.d.ts +29 -0
- package/dist/extensions/taskbar.js +64 -0
- package/dist/extensions/types.d.ts +182 -0
- package/dist/extensions/types.js +8 -0
- package/dist/info-panel.d.ts +2 -1
- package/dist/info-panel.js +78 -87
- package/dist/keybindings.d.ts +1 -1
- package/dist/keybindings.js +1 -0
- package/dist/layout-worker.d.ts +4 -1
- package/dist/layout-worker.js +51 -1
- package/dist/layout.d.ts +8 -0
- package/dist/layout.js +8 -1
- package/dist/main.js +216 -35
- package/dist/search.js +1 -1
- package/dist/server-api-routes.d.ts +56 -0
- package/dist/server-api-routes.js +442 -0
- package/dist/server-extensions.d.ts +126 -0
- package/dist/server-extensions.js +272 -0
- package/dist/server-viewer-state.d.ts +18 -0
- package/dist/server-viewer-state.js +33 -0
- package/dist/shortcuts.js +6 -2
- package/dist/sidebar.js +19 -7
- package/dist/style.css +356 -74
- package/dist/tools-pane.js +31 -14
- package/package.json +4 -3
- package/dist/app/assets/index-B3z5bBGl.css +0 -1
- package/dist/app/assets/index-CKYlU1zT.js +0 -35
- package/dist/app/assets/layout-worker-BZXiBoiC.js +0 -1
package/dist/main.js
CHANGED
|
@@ -2,14 +2,20 @@ import { listOntologies, loadOntology, saveOntology, renameOntology, listBranche
|
|
|
2
2
|
import { initSidebar } from "./sidebar";
|
|
3
3
|
import { initCanvas } from "./canvas";
|
|
4
4
|
import { initInfoPanel } from "./info-panel";
|
|
5
|
+
import { createPanelMount } from "./extensions/panel-mount";
|
|
5
6
|
import { initSearch } from "./search";
|
|
6
7
|
import { initToolsPane } from "./tools-pane";
|
|
7
8
|
import { setLayoutParams, getLayoutParams, autoLayoutParams } from "./layout";
|
|
8
9
|
import { initShortcuts } from "./shortcuts";
|
|
9
10
|
import { initEmptyState } from "./empty-state";
|
|
11
|
+
import { showToast } from "./dialog";
|
|
10
12
|
import { createHistory } from "./history";
|
|
11
13
|
import { matchKey } from "./keybindings";
|
|
12
14
|
import { initContextMenu } from "./context-menu";
|
|
15
|
+
import { initCopyPromptButton } from "./copy-prompt";
|
|
16
|
+
import { publishViewerState } from "./bridge";
|
|
17
|
+
import { createEventBus } from "./extensions/event-bus";
|
|
18
|
+
import { loadExtensions } from "./extensions/loader";
|
|
13
19
|
import defaultConfig from "./default-config.json";
|
|
14
20
|
import "./style.css";
|
|
15
21
|
let activeOntology = "";
|
|
@@ -68,6 +74,7 @@ async function main() {
|
|
|
68
74
|
// Refresh sidebar counts
|
|
69
75
|
const updated = await listOntologies();
|
|
70
76
|
sidebar.setSummaries(updated);
|
|
77
|
+
eventBus.emit("graph-changed");
|
|
71
78
|
}
|
|
72
79
|
/** Snapshot current state, then save. Call this instead of save() for undoable actions. */
|
|
73
80
|
async function undoableSave() {
|
|
@@ -88,7 +95,11 @@ async function main() {
|
|
|
88
95
|
// canvas is used inside the navigate callback but declared below —
|
|
89
96
|
// that's fine because the callback is only invoked after setup completes.
|
|
90
97
|
let canvas;
|
|
91
|
-
|
|
98
|
+
// Create the shared panel-mount up front. Both info-panel and the
|
|
99
|
+
// extension loader use this single instance so all panels share the
|
|
100
|
+
// same click-to-front z-stack and the persistent layer DOM element.
|
|
101
|
+
const panelMount = createPanelMount(canvasContainer);
|
|
102
|
+
const infoPanel = initInfoPanel(canvasContainer, panelMount, {
|
|
92
103
|
onUpdateNode(nodeId, properties) {
|
|
93
104
|
if (!currentData)
|
|
94
105
|
return;
|
|
@@ -215,7 +226,7 @@ async function main() {
|
|
|
215
226
|
pathBar.className = "path-bar hidden";
|
|
216
227
|
canvasContainer.appendChild(pathBar);
|
|
217
228
|
function showPathBar(path) {
|
|
218
|
-
pathBar.
|
|
229
|
+
pathBar.replaceChildren();
|
|
219
230
|
if (!currentData)
|
|
220
231
|
return;
|
|
221
232
|
for (let i = 0; i < path.nodeIds.length; i++) {
|
|
@@ -248,11 +259,27 @@ async function main() {
|
|
|
248
259
|
}
|
|
249
260
|
function hidePathBar() {
|
|
250
261
|
pathBar.classList.add("hidden");
|
|
251
|
-
pathBar.
|
|
262
|
+
pathBar.replaceChildren();
|
|
252
263
|
canvas.clearHighlightedPath();
|
|
253
264
|
}
|
|
265
|
+
// Event bus for extensions. Emitted from the same hooks that drive
|
|
266
|
+
// bridge publishing — selection changes, focus enter/exit, graph
|
|
267
|
+
// load, save. Subscribers run synchronously; errors in one don't
|
|
268
|
+
// affect others.
|
|
269
|
+
const eventBus = createEventBus();
|
|
270
|
+
function publishBridgeState() {
|
|
271
|
+
if (!activeOntology)
|
|
272
|
+
return;
|
|
273
|
+
publishViewerState({
|
|
274
|
+
graph: activeOntology,
|
|
275
|
+
selection: currentSelection,
|
|
276
|
+
focus: canvas?.getFocusInfo() ?? null,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
254
279
|
canvas = initCanvas(canvasContainer, (nodeIds) => {
|
|
255
280
|
currentSelection = nodeIds ?? [];
|
|
281
|
+
publishBridgeState();
|
|
282
|
+
eventBus.emit("selection-changed");
|
|
256
283
|
// Don't touch the path bar when walk mode is active — syncWalkTrail manages it
|
|
257
284
|
if (!canvas.getWalkMode()) {
|
|
258
285
|
if (nodeIds && nodeIds.length === 2) {
|
|
@@ -297,6 +324,8 @@ async function main() {
|
|
|
297
324
|
updateUrl(activeOntology);
|
|
298
325
|
syncWalkTrail();
|
|
299
326
|
}
|
|
327
|
+
publishBridgeState();
|
|
328
|
+
eventBus.emit("focus-changed");
|
|
300
329
|
}, { lod: cfg.lod, navigation: cfg.navigation, walk: cfg.walk });
|
|
301
330
|
const search = initSearch(canvasContainer, {
|
|
302
331
|
maxResults: cfg.limits.maxSearchResults,
|
|
@@ -447,15 +476,46 @@ async function main() {
|
|
|
447
476
|
const toolsToggle = canvasContainer.querySelector(".tools-pane-toggle");
|
|
448
477
|
if (toolsToggle)
|
|
449
478
|
topLeft.appendChild(toolsToggle);
|
|
450
|
-
// Move search overlay into center slot
|
|
451
|
-
const searchOverlay = canvasContainer.querySelector(".search-overlay");
|
|
452
|
-
if (searchOverlay)
|
|
453
|
-
topCenter.appendChild(searchOverlay);
|
|
454
479
|
// Move zoom controls and theme toggle into right slot
|
|
455
480
|
const zoomControls = canvasContainer.querySelector(".zoom-controls");
|
|
456
481
|
if (zoomControls)
|
|
457
482
|
topRight.appendChild(zoomControls);
|
|
458
483
|
topRight.appendChild(themeBtn);
|
|
484
|
+
// Extension taskbar slots — four hosted containers, one per
|
|
485
|
+
// supported icon position. The top slots flank the search overlay
|
|
486
|
+
// INSIDE top-center, alongside the copy-prompt button. The viewer's
|
|
487
|
+
// own controls in top-left/top-right (zoom, theme, tools toggle)
|
|
488
|
+
// stay visually separated. Bottom slots float in the canvas
|
|
489
|
+
// corners. All four start hidden — they're invisible until at
|
|
490
|
+
// least one extension registers into them, so empty slots take no
|
|
491
|
+
// space.
|
|
492
|
+
const extSlotTopLeft = document.createElement("div");
|
|
493
|
+
extSlotTopLeft.className = "ext-slot ext-slot-top-left";
|
|
494
|
+
topCenter.appendChild(extSlotTopLeft);
|
|
495
|
+
// Move search overlay into center slot, after the left ext slot so
|
|
496
|
+
// top-center reads: [ext-left, search, ext-right, copy-prompt].
|
|
497
|
+
const searchOverlay = canvasContainer.querySelector(".search-overlay");
|
|
498
|
+
if (searchOverlay)
|
|
499
|
+
topCenter.appendChild(searchOverlay);
|
|
500
|
+
const extSlotTopRight = document.createElement("div");
|
|
501
|
+
extSlotTopRight.className = "ext-slot ext-slot-top-right";
|
|
502
|
+
topCenter.appendChild(extSlotTopRight);
|
|
503
|
+
// Copy-prompt button — viewer-owned, sits at the rightmost end of
|
|
504
|
+
// the top-center group so any registered ext-right icons (like
|
|
505
|
+
// chat) appear immediately to its left.
|
|
506
|
+
const copyPromptBtn = initCopyPromptButton(() => ({
|
|
507
|
+
graphName: activeOntology,
|
|
508
|
+
data: currentData,
|
|
509
|
+
selection: currentSelection,
|
|
510
|
+
focus: canvas.getFocusInfo(),
|
|
511
|
+
}));
|
|
512
|
+
topCenter.appendChild(copyPromptBtn);
|
|
513
|
+
const extSlotBottomLeft = document.createElement("div");
|
|
514
|
+
extSlotBottomLeft.className = "ext-slot ext-slot-bottom-left";
|
|
515
|
+
canvasContainer.appendChild(extSlotBottomLeft);
|
|
516
|
+
const extSlotBottomRight = document.createElement("div");
|
|
517
|
+
extSlotBottomRight.className = "ext-slot ext-slot-bottom-right";
|
|
518
|
+
canvasContainer.appendChild(extSlotBottomRight);
|
|
459
519
|
topBar.appendChild(topLeft);
|
|
460
520
|
topBar.appendChild(topCenter);
|
|
461
521
|
topBar.appendChild(topRight);
|
|
@@ -732,6 +792,9 @@ async function main() {
|
|
|
732
792
|
toolsPane.setData(currentData);
|
|
733
793
|
emptyState.hide();
|
|
734
794
|
updateUrl(name);
|
|
795
|
+
publishBridgeState();
|
|
796
|
+
eventBus.emit("graph-switched");
|
|
797
|
+
eventBus.emit("graph-changed");
|
|
735
798
|
// Load branches and snapshots — skipped for remote graphs (read-only,
|
|
736
799
|
// no branch/snapshot/snippet APIs on the remote endpoint)
|
|
737
800
|
if (!activeIsRemote) {
|
|
@@ -782,31 +845,129 @@ async function main() {
|
|
|
782
845
|
}
|
|
783
846
|
})
|
|
784
847
|
.catch(() => { });
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
848
|
+
// --- Share link detection ---
|
|
849
|
+
// If URL has ?share=TOKEN, load from the relay instead of local API.
|
|
850
|
+
// The #k=KEY fragment (if present) is used for client-side decryption.
|
|
851
|
+
const shareParams = new URLSearchParams(window.location.search);
|
|
852
|
+
const shareToken = shareParams.get("share");
|
|
853
|
+
if (shareToken) {
|
|
854
|
+
try {
|
|
855
|
+
const metaRes = await fetch(`/v1/share/${shareToken}/meta`);
|
|
856
|
+
if (!metaRes.ok)
|
|
857
|
+
throw new Error("Share link not found or expired");
|
|
858
|
+
const meta = await metaRes.json();
|
|
859
|
+
const dataRes = await fetch(`/v1/share/${shareToken}`);
|
|
860
|
+
if (!dataRes.ok)
|
|
861
|
+
throw new Error("Failed to download shared backpack");
|
|
862
|
+
// Parse the BPAK envelope (inline — avoids pulling Node.js deps from backpack-ontology)
|
|
863
|
+
const envelopeBytes = new Uint8Array(await dataRes.arrayBuffer());
|
|
864
|
+
if (envelopeBytes.length < 9 || envelopeBytes[0] !== 0x42 || envelopeBytes[1] !== 0x50 || envelopeBytes[2] !== 0x41 || envelopeBytes[3] !== 0x4B) {
|
|
865
|
+
throw new Error("Invalid share data: not a BPAK envelope");
|
|
866
|
+
}
|
|
867
|
+
const headerLen = new DataView(envelopeBytes.buffer, envelopeBytes.byteOffset, envelopeBytes.byteLength).getUint32(5, false);
|
|
868
|
+
if (9 + headerLen > envelopeBytes.length)
|
|
869
|
+
throw new Error("Invalid envelope: header length exceeds data");
|
|
870
|
+
const envelopeHeader = JSON.parse(new TextDecoder().decode(envelopeBytes.slice(9, 9 + headerLen)));
|
|
871
|
+
const envelopePayload = envelopeBytes.slice(9 + headerLen);
|
|
872
|
+
let graphData;
|
|
873
|
+
if (envelopeHeader.format !== "plaintext") {
|
|
874
|
+
// Encrypted — decrypt client-side using fragment key
|
|
875
|
+
const fragment = window.location.hash.slice(1);
|
|
876
|
+
const keyParam = new URLSearchParams(fragment).get("k") ?? fragment.split("k=")[1];
|
|
877
|
+
if (!keyParam)
|
|
878
|
+
throw new Error("Missing decryption key in URL fragment");
|
|
879
|
+
const { Decrypter } = await import("age-encryption");
|
|
880
|
+
const secretKey = atob(keyParam.replace(/-/g, "+").replace(/_/g, "/"));
|
|
881
|
+
const d = new Decrypter();
|
|
882
|
+
d.addIdentity(secretKey);
|
|
883
|
+
const plaintext = await d.decrypt(envelopePayload);
|
|
884
|
+
graphData = JSON.parse(new TextDecoder().decode(plaintext));
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
graphData = JSON.parse(new TextDecoder().decode(envelopePayload));
|
|
888
|
+
}
|
|
889
|
+
// Render in read-only mode
|
|
890
|
+
activeOntology = meta.backpack_name || "Shared Backpack";
|
|
891
|
+
currentData = graphData;
|
|
892
|
+
canvas.loadGraph(graphData);
|
|
893
|
+
search.setLearningGraphData(graphData);
|
|
894
|
+
sidebar.setSummaries([{
|
|
895
|
+
name: activeOntology,
|
|
896
|
+
description: "",
|
|
897
|
+
nodeCount: graphData.nodes?.length ?? 0,
|
|
898
|
+
edgeCount: graphData.edges?.length ?? 0,
|
|
899
|
+
nodeTypes: [],
|
|
900
|
+
}]);
|
|
901
|
+
sidebar.setActive(activeOntology);
|
|
902
|
+
document.title = `${activeOntology} — Backpack`;
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
const msg = err instanceof Error ? err.message : "Failed to load shared backpack";
|
|
906
|
+
showToast(msg, 5000);
|
|
907
|
+
emptyState.show();
|
|
908
|
+
}
|
|
806
909
|
}
|
|
807
910
|
else {
|
|
808
|
-
|
|
911
|
+
// Normal mode — load from local/cloud API
|
|
912
|
+
const [summaries, remotes] = await Promise.all([
|
|
913
|
+
listOntologies(),
|
|
914
|
+
listRemotes().catch(() => []),
|
|
915
|
+
]);
|
|
916
|
+
sidebar.setSummaries(summaries);
|
|
917
|
+
sidebar.setRemotes(remotes);
|
|
918
|
+
remoteNames = new Set(remotes.map((r) => r.name));
|
|
919
|
+
// Auto-load from URL hash, or first graph
|
|
920
|
+
const initialUrl = parseUrl();
|
|
921
|
+
const initialName = initialUrl.graph && summaries.some((s) => s.name === initialUrl.graph)
|
|
922
|
+
? initialUrl.graph
|
|
923
|
+
: initialUrl.graph && remoteNames.has(initialUrl.graph)
|
|
924
|
+
? initialUrl.graph
|
|
925
|
+
: summaries.length > 0
|
|
926
|
+
? summaries[0].name
|
|
927
|
+
: remotes.length > 0
|
|
928
|
+
? remotes[0].name
|
|
929
|
+
: null;
|
|
930
|
+
if (initialName) {
|
|
931
|
+
await selectGraph(initialName, initialUrl.nodes.length ? initialUrl.nodes : undefined, initialUrl.focus.length ? initialUrl.focus : undefined, initialUrl.hops);
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
emptyState.show();
|
|
935
|
+
}
|
|
809
936
|
}
|
|
937
|
+
// --- Extension system ---
|
|
938
|
+
// Load extensions after the viewer is fully initialized so any
|
|
939
|
+
// extension that immediately reads the current graph state gets a
|
|
940
|
+
// populated graph (not a startup race).
|
|
941
|
+
const host = {
|
|
942
|
+
getGraph: () => currentData,
|
|
943
|
+
getGraphName: () => activeOntology,
|
|
944
|
+
getSelection: () => [...currentSelection],
|
|
945
|
+
getFocus: () => canvas.getFocusInfo(),
|
|
946
|
+
saveCurrentGraph: async () => {
|
|
947
|
+
await save();
|
|
948
|
+
},
|
|
949
|
+
snapshotForUndo: () => {
|
|
950
|
+
if (currentData)
|
|
951
|
+
undoHistory.push(currentData);
|
|
952
|
+
},
|
|
953
|
+
panToNode: (id) => canvas.panToNode(id),
|
|
954
|
+
focusNodes: (ids, hops) => canvas.enterFocus(ids, hops),
|
|
955
|
+
exitFocus: () => {
|
|
956
|
+
if (canvas.isFocused())
|
|
957
|
+
canvas.exitFocus();
|
|
958
|
+
},
|
|
959
|
+
taskbarSlots: {
|
|
960
|
+
topLeft: extSlotTopLeft,
|
|
961
|
+
topRight: extSlotTopRight,
|
|
962
|
+
bottomLeft: extSlotBottomLeft,
|
|
963
|
+
bottomRight: extSlotBottomRight,
|
|
964
|
+
},
|
|
965
|
+
subscribe: (event, cb) => eventBus.subscribe(event, cb),
|
|
966
|
+
};
|
|
967
|
+
// Fire-and-forget — extension loading errors don't block startup
|
|
968
|
+
loadExtensions(host, panelMount).catch((err) => {
|
|
969
|
+
console.error("[backpack-viewer] extension loader failed:", err);
|
|
970
|
+
});
|
|
810
971
|
// Keyboard shortcuts — dispatched via configurable bindings
|
|
811
972
|
const actions = {
|
|
812
973
|
search() { search.focus(); },
|
|
@@ -873,6 +1034,11 @@ async function main() {
|
|
|
873
1034
|
clusteringIncrease() { const p = getLayoutParams(); setLayoutParams({ clusterStrength: Math.min(1, p.clusterStrength + 0.03) }); canvas.reheat(); },
|
|
874
1035
|
help() { shortcuts.toggle(); },
|
|
875
1036
|
toggleSidebar() { sidebar.toggle(); },
|
|
1037
|
+
resetPins() {
|
|
1038
|
+
const released = canvas.releaseAllPins();
|
|
1039
|
+
if (released)
|
|
1040
|
+
showToast("Manual layout reset — pins released");
|
|
1041
|
+
},
|
|
876
1042
|
walkIsolate() {
|
|
877
1043
|
if (!currentData)
|
|
878
1044
|
return;
|
|
@@ -893,12 +1059,18 @@ async function main() {
|
|
|
893
1059
|
walkBtn.classList.toggle("active", canvas.getWalkMode());
|
|
894
1060
|
syncWalkTrail();
|
|
895
1061
|
},
|
|
896
|
-
escape() {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1062
|
+
escape() {
|
|
1063
|
+
// Priority: 1. clear selection, 2. exit focus, 3. hide help modal
|
|
1064
|
+
if (canvas.getSelectedNodeIds().length > 0) {
|
|
1065
|
+
canvas.clearSelection();
|
|
1066
|
+
}
|
|
1067
|
+
else if (canvas.isFocused()) {
|
|
1068
|
+
toolsPane.clearFocusSet();
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
shortcuts.hide();
|
|
1072
|
+
}
|
|
1073
|
+
},
|
|
902
1074
|
};
|
|
903
1075
|
document.addEventListener("keydown", (e) => {
|
|
904
1076
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
|
|
@@ -940,6 +1112,12 @@ async function main() {
|
|
|
940
1112
|
await refreshBackpacksAndGraphs();
|
|
941
1113
|
});
|
|
942
1114
|
import.meta.hot.on("ontology-change", async () => {
|
|
1115
|
+
// If the user had manually pinned nodes, the incoming data change
|
|
1116
|
+
// is going to reset their layout tweaks. Warn them so they know
|
|
1117
|
+
// the change wasn't their fault — this is the one involuntary
|
|
1118
|
+
// pin-release case (graph switches and focus toggles are all
|
|
1119
|
+
// user-initiated and don't need a toast).
|
|
1120
|
+
const hadPins = canvas.hasPinnedNodes();
|
|
943
1121
|
const [updated, updatedRemotes] = await Promise.all([
|
|
944
1122
|
listOntologies(),
|
|
945
1123
|
listRemotes().catch(() => []),
|
|
@@ -971,6 +1149,9 @@ async function main() {
|
|
|
971
1149
|
search.setLearningGraphData(currentData);
|
|
972
1150
|
toolsPane.setData(currentData);
|
|
973
1151
|
}
|
|
1152
|
+
if (hadPins) {
|
|
1153
|
+
showToast("Manual layout reset — new data arrived");
|
|
1154
|
+
}
|
|
974
1155
|
});
|
|
975
1156
|
}
|
|
976
1157
|
}
|
package/dist/search.js
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type JsonFileBackend, type RemoteRegistry } from "backpack-ontology";
|
|
3
|
+
import type { ViewerConfig } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Shared API route handler. Both `bin/serve.js` (production raw http)
|
|
6
|
+
* and `vite.config.ts`'s middleware plugin (dev) call `handleApiRequest`
|
|
7
|
+
* with the raw Node IncomingMessage/ServerResponse — they share the
|
|
8
|
+
* exact same shape. Each entry only owns the static-file serving and
|
|
9
|
+
* its own startup wiring.
|
|
10
|
+
*
|
|
11
|
+
* Before this module existed, every route below had two near-identical
|
|
12
|
+
* copies (one per entry file) and adding/changing an endpoint required
|
|
13
|
+
* editing both, with predictable drift bugs. Consolidated here into a
|
|
14
|
+
* single source of truth.
|
|
15
|
+
*
|
|
16
|
+
* Storage handling: the active backpack can be swapped at runtime via
|
|
17
|
+
* `/api/backpacks/switch`, which atomically replaces the storage
|
|
18
|
+
* backend. The context holds a mutable wrapper so the swap is visible
|
|
19
|
+
* to subsequent requests. Vite needs to broadcast a WebSocket event on
|
|
20
|
+
* the swap; production has no WS channel, so the hook is optional.
|
|
21
|
+
*/
|
|
22
|
+
export interface BackpackEntry {
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
color: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ApiContext {
|
|
28
|
+
/** Mutable wrapper around the storage backend so backpack-switch can swap it. */
|
|
29
|
+
storage: {
|
|
30
|
+
current: JsonFileBackend;
|
|
31
|
+
activeEntry: BackpackEntry | null;
|
|
32
|
+
};
|
|
33
|
+
remoteRegistry: RemoteRegistry;
|
|
34
|
+
viewerConfig: ViewerConfig;
|
|
35
|
+
/** Recreate the backend pointing at the active backpack. */
|
|
36
|
+
makeBackend: () => Promise<{
|
|
37
|
+
backend: JsonFileBackend;
|
|
38
|
+
entry: BackpackEntry;
|
|
39
|
+
}>;
|
|
40
|
+
/** Optional hook called after a successful backpack switch (vite uses this for WS broadcast). */
|
|
41
|
+
onActiveBackpackChange?: () => void;
|
|
42
|
+
/** How to answer GET /api/version-check. Differs between dev (always not-stale) and prod (cached npm lookup). */
|
|
43
|
+
versionCheck: () => Promise<{
|
|
44
|
+
current: string;
|
|
45
|
+
latest: string | null;
|
|
46
|
+
stale: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Try to match and handle an API request. Returns true if the request
|
|
51
|
+
* was handled (response written), false if no route matched and the
|
|
52
|
+
* caller should fall through to its own handlers (e.g., static files).
|
|
53
|
+
*
|
|
54
|
+
* Errors that escape route handlers result in a 500 with a JSON body.
|
|
55
|
+
*/
|
|
56
|
+
export declare function handleApiRequest(req: IncomingMessage, res: ServerResponse, ctx: ApiContext): Promise<boolean>;
|