backpack-viewer 0.3.1 → 0.4.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 +116 -3
- package/dist/app/assets/index-DBZCyAjY.js +34 -0
- package/dist/app/assets/{index-CvkozBSE.css → index-DlVz8Lz7.css} +1 -1
- package/dist/app/index.html +2 -2
- package/dist/main.js +57 -0
- package/dist/sidebar.d.ts +11 -0
- package/dist/sidebar.js +136 -0
- package/dist/style.css +126 -0
- package/package.json +2 -2
- package/dist/app/assets/index-j3SowZae.js +0 -34
package/dist/main.js
CHANGED
|
@@ -517,7 +517,50 @@ async function main() {
|
|
|
517
517
|
await deleteSnippet(graphName, snippetId);
|
|
518
518
|
await refreshSnippets(graphName);
|
|
519
519
|
},
|
|
520
|
+
onBackpackSwitch: async (name) => {
|
|
521
|
+
await fetch("/api/backpacks/switch", {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: { "Content-Type": "application/json" },
|
|
524
|
+
body: JSON.stringify({ name }),
|
|
525
|
+
});
|
|
526
|
+
// The dev WS will fire an active-backpack-change event that
|
|
527
|
+
// triggers refreshBackpacksAndGraphs(). Production server has no
|
|
528
|
+
// live-reload channel, so we refresh immediately as fallback.
|
|
529
|
+
await refreshBackpacksAndGraphs();
|
|
530
|
+
},
|
|
531
|
+
onBackpackRegister: async (name, p, activate) => {
|
|
532
|
+
await fetch("/api/backpacks", {
|
|
533
|
+
method: "POST",
|
|
534
|
+
headers: { "Content-Type": "application/json" },
|
|
535
|
+
body: JSON.stringify({ name, path: p, activate }),
|
|
536
|
+
});
|
|
537
|
+
await refreshBackpacksAndGraphs();
|
|
538
|
+
},
|
|
520
539
|
});
|
|
540
|
+
async function refreshBackpacksAndGraphs() {
|
|
541
|
+
try {
|
|
542
|
+
const res = await fetch("/api/backpacks");
|
|
543
|
+
const list = await res.json();
|
|
544
|
+
sidebar.setBackpacks(list);
|
|
545
|
+
}
|
|
546
|
+
catch { }
|
|
547
|
+
// Re-fetch ontology list from the (possibly new) active backpack
|
|
548
|
+
try {
|
|
549
|
+
const updated = await listOntologies();
|
|
550
|
+
sidebar.setSummaries(updated);
|
|
551
|
+
// If the previously-active graph no longer exists, clear it
|
|
552
|
+
if (activeOntology && !updated.some((g) => g.name === activeOntology)) {
|
|
553
|
+
activeOntology = "";
|
|
554
|
+
currentData = null;
|
|
555
|
+
canvas.loadGraph({
|
|
556
|
+
metadata: { name: "", description: "", createdAt: "", updatedAt: "" },
|
|
557
|
+
nodes: [],
|
|
558
|
+
edges: [],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch { }
|
|
563
|
+
}
|
|
521
564
|
function syncWalkTrail() {
|
|
522
565
|
const trail = canvas.getWalkTrail();
|
|
523
566
|
if (!currentData || trail.length === 0) {
|
|
@@ -719,6 +762,15 @@ async function main() {
|
|
|
719
762
|
}
|
|
720
763
|
}
|
|
721
764
|
}
|
|
765
|
+
// Fetch backpack registry first so the picker shows the right active
|
|
766
|
+
// backpack from the initial render. Fire-and-forget ontology + remote
|
|
767
|
+
// list in parallel.
|
|
768
|
+
try {
|
|
769
|
+
const res = await fetch("/api/backpacks");
|
|
770
|
+
const list = await res.json();
|
|
771
|
+
sidebar.setBackpacks(list);
|
|
772
|
+
}
|
|
773
|
+
catch { }
|
|
722
774
|
// Load ontology list (local + remote in parallel)
|
|
723
775
|
const [summaries, remotes] = await Promise.all([
|
|
724
776
|
listOntologies(),
|
|
@@ -871,6 +923,11 @@ async function main() {
|
|
|
871
923
|
});
|
|
872
924
|
// Live reload — when Claude adds nodes via MCP, re-fetch and re-render
|
|
873
925
|
if (import.meta.hot) {
|
|
926
|
+
// Hot-swap when the active backpack changes (via /api/backpacks/switch
|
|
927
|
+
// or the user clicking the picker on another viewer tab)
|
|
928
|
+
import.meta.hot.on("active-backpack-change", async () => {
|
|
929
|
+
await refreshBackpacksAndGraphs();
|
|
930
|
+
});
|
|
874
931
|
import.meta.hot.on("ontology-change", async () => {
|
|
875
932
|
const [updated, updatedRemotes] = await Promise.all([
|
|
876
933
|
listOntologies(),
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { LearningGraphSummary } from "backpack-ontology";
|
|
2
2
|
import type { RemoteSummary } from "./api.js";
|
|
3
|
+
export interface BackpackSummary {
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
color: string;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
}
|
|
3
9
|
export interface SidebarCallbacks {
|
|
4
10
|
onSelect: (name: string) => void;
|
|
5
11
|
onRename?: (oldName: string, newName: string) => void;
|
|
@@ -8,8 +14,13 @@ export interface SidebarCallbacks {
|
|
|
8
14
|
onBranchDelete?: (graphName: string, branchName: string) => void;
|
|
9
15
|
onSnippetLoad?: (graphName: string, snippetId: string) => void;
|
|
10
16
|
onSnippetDelete?: (graphName: string, snippetId: string) => void;
|
|
17
|
+
onBackpackSwitch?: (name: string) => void;
|
|
18
|
+
onBackpackRegister?: (name: string, path: string, activate: boolean) => void;
|
|
11
19
|
}
|
|
12
20
|
export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
|
|
21
|
+
setBackpacks(list: BackpackSummary[]): void;
|
|
22
|
+
setActiveBackpack(entry: BackpackSummary): void;
|
|
23
|
+
getActiveBackpack(): BackpackSummary | null;
|
|
13
24
|
setSummaries(summaries: LearningGraphSummary[]): void;
|
|
14
25
|
setActive(name: string): void;
|
|
15
26
|
setRemotes(remotes: RemoteSummary[]): void;
|
package/dist/sidebar.js
CHANGED
|
@@ -51,6 +51,112 @@ export function initSidebar(container, onSelectOrCallbacks) {
|
|
|
51
51
|
headingRow.appendChild(heading);
|
|
52
52
|
headingRow.appendChild(collapseBtn);
|
|
53
53
|
container.appendChild(headingRow);
|
|
54
|
+
// Backpack picker pill — discrete indicator of the active backpack with
|
|
55
|
+
// a dropdown to switch between registered ones.
|
|
56
|
+
const backpackPicker = document.createElement("button");
|
|
57
|
+
backpackPicker.className = "backpack-picker-pill";
|
|
58
|
+
backpackPicker.type = "button";
|
|
59
|
+
backpackPicker.setAttribute("aria-haspopup", "listbox");
|
|
60
|
+
backpackPicker.setAttribute("aria-expanded", "false");
|
|
61
|
+
const pickerDot = document.createElement("span");
|
|
62
|
+
pickerDot.className = "backpack-picker-dot";
|
|
63
|
+
const pickerName = document.createElement("span");
|
|
64
|
+
pickerName.className = "backpack-picker-name";
|
|
65
|
+
pickerName.textContent = "...";
|
|
66
|
+
const pickerCaret = document.createElement("span");
|
|
67
|
+
pickerCaret.className = "backpack-picker-caret";
|
|
68
|
+
pickerCaret.textContent = "▾";
|
|
69
|
+
backpackPicker.appendChild(pickerDot);
|
|
70
|
+
backpackPicker.appendChild(pickerName);
|
|
71
|
+
backpackPicker.appendChild(pickerCaret);
|
|
72
|
+
const pickerDropdown = document.createElement("div");
|
|
73
|
+
pickerDropdown.className = "backpack-picker-dropdown";
|
|
74
|
+
pickerDropdown.hidden = true;
|
|
75
|
+
pickerDropdown.setAttribute("role", "listbox");
|
|
76
|
+
const pickerContainer = document.createElement("div");
|
|
77
|
+
pickerContainer.className = "backpack-picker-container";
|
|
78
|
+
pickerContainer.appendChild(backpackPicker);
|
|
79
|
+
pickerContainer.appendChild(pickerDropdown);
|
|
80
|
+
container.appendChild(pickerContainer);
|
|
81
|
+
let pickerOpen = false;
|
|
82
|
+
function closePicker() {
|
|
83
|
+
pickerOpen = false;
|
|
84
|
+
pickerDropdown.hidden = true;
|
|
85
|
+
backpackPicker.setAttribute("aria-expanded", "false");
|
|
86
|
+
}
|
|
87
|
+
function openPicker() {
|
|
88
|
+
pickerOpen = true;
|
|
89
|
+
pickerDropdown.hidden = false;
|
|
90
|
+
backpackPicker.setAttribute("aria-expanded", "true");
|
|
91
|
+
}
|
|
92
|
+
backpackPicker.addEventListener("click", (e) => {
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
if (pickerOpen)
|
|
95
|
+
closePicker();
|
|
96
|
+
else
|
|
97
|
+
openPicker();
|
|
98
|
+
});
|
|
99
|
+
// Click outside closes the dropdown
|
|
100
|
+
document.addEventListener("click", (e) => {
|
|
101
|
+
if (!pickerContainer.contains(e.target))
|
|
102
|
+
closePicker();
|
|
103
|
+
});
|
|
104
|
+
let currentBackpacks = [];
|
|
105
|
+
let currentActiveBackpack = null;
|
|
106
|
+
function renderPickerDropdown() {
|
|
107
|
+
pickerDropdown.replaceChildren();
|
|
108
|
+
for (const b of currentBackpacks) {
|
|
109
|
+
const item = document.createElement("button");
|
|
110
|
+
item.className = "backpack-picker-item";
|
|
111
|
+
item.type = "button";
|
|
112
|
+
item.setAttribute("role", "option");
|
|
113
|
+
if (b.active)
|
|
114
|
+
item.classList.add("active");
|
|
115
|
+
const dot = document.createElement("span");
|
|
116
|
+
dot.className = "backpack-picker-item-dot";
|
|
117
|
+
dot.style.setProperty("--backpack-color", b.color);
|
|
118
|
+
const name = document.createElement("span");
|
|
119
|
+
name.className = "backpack-picker-item-name";
|
|
120
|
+
name.textContent = b.name;
|
|
121
|
+
const path = document.createElement("span");
|
|
122
|
+
path.className = "backpack-picker-item-path";
|
|
123
|
+
path.textContent = b.path;
|
|
124
|
+
item.appendChild(dot);
|
|
125
|
+
item.appendChild(name);
|
|
126
|
+
item.appendChild(path);
|
|
127
|
+
item.addEventListener("click", (e) => {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
closePicker();
|
|
130
|
+
if (!b.active && cbs.onBackpackSwitch) {
|
|
131
|
+
cbs.onBackpackSwitch(b.name);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
pickerDropdown.appendChild(item);
|
|
135
|
+
}
|
|
136
|
+
// Separator + "Add new backpack..." action
|
|
137
|
+
const divider = document.createElement("div");
|
|
138
|
+
divider.className = "backpack-picker-divider";
|
|
139
|
+
pickerDropdown.appendChild(divider);
|
|
140
|
+
const addItem = document.createElement("button");
|
|
141
|
+
addItem.className = "backpack-picker-item backpack-picker-add";
|
|
142
|
+
addItem.type = "button";
|
|
143
|
+
addItem.textContent = "+ Add new backpack…";
|
|
144
|
+
addItem.addEventListener("click", async (e) => {
|
|
145
|
+
e.stopPropagation();
|
|
146
|
+
closePicker();
|
|
147
|
+
if (!cbs.onBackpackRegister)
|
|
148
|
+
return;
|
|
149
|
+
const name = await showPrompt("Backpack name", "Short kebab-case name (e.g. work, family, project-alpha)", "");
|
|
150
|
+
if (!name)
|
|
151
|
+
return;
|
|
152
|
+
const p = await showPrompt("Backpack path", "Absolute or tilde-expanded path to the graphs directory", "");
|
|
153
|
+
if (!p)
|
|
154
|
+
return;
|
|
155
|
+
const activate = await showConfirm("Switch to new backpack?", `Make "${name}" the active backpack immediately?`);
|
|
156
|
+
cbs.onBackpackRegister(name, p, activate);
|
|
157
|
+
});
|
|
158
|
+
pickerDropdown.appendChild(addItem);
|
|
159
|
+
}
|
|
54
160
|
// Expand button — inserted into the canvas top-left bar when sidebar is collapsed
|
|
55
161
|
const expandBtn = document.createElement("button");
|
|
56
162
|
expandBtn.className = "tools-pane-toggle hidden";
|
|
@@ -79,6 +185,36 @@ export function initSidebar(container, onSelectOrCallbacks) {
|
|
|
79
185
|
}
|
|
80
186
|
});
|
|
81
187
|
return {
|
|
188
|
+
setBackpacks(list) {
|
|
189
|
+
currentBackpacks = list.slice();
|
|
190
|
+
const active = list.find((b) => b.active) ?? null;
|
|
191
|
+
currentActiveBackpack = active;
|
|
192
|
+
if (active) {
|
|
193
|
+
pickerName.textContent = active.name;
|
|
194
|
+
pickerDot.style.setProperty("--backpack-color", active.color);
|
|
195
|
+
container.style.setProperty("--backpack-color", active.color);
|
|
196
|
+
}
|
|
197
|
+
renderPickerDropdown();
|
|
198
|
+
},
|
|
199
|
+
setActiveBackpack(entry) {
|
|
200
|
+
currentActiveBackpack = entry;
|
|
201
|
+
// Update the currentBackpacks list to reflect the new active
|
|
202
|
+
currentBackpacks = currentBackpacks.map((b) => ({
|
|
203
|
+
...b,
|
|
204
|
+
active: b.name === entry.name,
|
|
205
|
+
}));
|
|
206
|
+
// If this name wasn't in the list (newly registered), include it
|
|
207
|
+
if (!currentBackpacks.some((b) => b.name === entry.name)) {
|
|
208
|
+
currentBackpacks.push({ ...entry, active: true });
|
|
209
|
+
}
|
|
210
|
+
pickerName.textContent = entry.name;
|
|
211
|
+
pickerDot.style.setProperty("--backpack-color", entry.color);
|
|
212
|
+
container.style.setProperty("--backpack-color", entry.color);
|
|
213
|
+
renderPickerDropdown();
|
|
214
|
+
},
|
|
215
|
+
getActiveBackpack() {
|
|
216
|
+
return currentActiveBackpack;
|
|
217
|
+
},
|
|
82
218
|
setSummaries(summaries) {
|
|
83
219
|
list.innerHTML = "";
|
|
84
220
|
// Fetch all locks in one batch request, then distribute to items
|
package/dist/style.css
CHANGED
|
@@ -110,6 +110,9 @@ body {
|
|
|
110
110
|
min-width: 280px;
|
|
111
111
|
background: var(--bg-surface);
|
|
112
112
|
border-right: 1px solid var(--border);
|
|
113
|
+
/* Colored left border accents the active backpack — color is set via
|
|
114
|
+
CSS custom property --backpack-color updated on switch */
|
|
115
|
+
border-left: 3px solid var(--backpack-color, transparent);
|
|
113
116
|
display: flex;
|
|
114
117
|
flex-direction: column;
|
|
115
118
|
padding: 16px;
|
|
@@ -315,6 +318,129 @@ body {
|
|
|
315
318
|
opacity: 1;
|
|
316
319
|
}
|
|
317
320
|
|
|
321
|
+
/* --- Backpack picker (active backpack indicator + switcher) --- */
|
|
322
|
+
|
|
323
|
+
.backpack-picker-container {
|
|
324
|
+
position: relative;
|
|
325
|
+
margin-bottom: 10px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.backpack-picker-pill {
|
|
329
|
+
display: flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: 6px;
|
|
332
|
+
width: 100%;
|
|
333
|
+
padding: 4px 8px;
|
|
334
|
+
background: var(--bg-base);
|
|
335
|
+
border: 1px solid var(--border);
|
|
336
|
+
border-radius: 999px;
|
|
337
|
+
color: var(--fg);
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
font-family: inherit;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
text-align: left;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.backpack-picker-pill:hover {
|
|
345
|
+
border-color: var(--backpack-color, var(--accent));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.backpack-picker-dot {
|
|
349
|
+
display: inline-block;
|
|
350
|
+
width: 8px;
|
|
351
|
+
height: 8px;
|
|
352
|
+
border-radius: 50%;
|
|
353
|
+
background: var(--backpack-color, var(--accent));
|
|
354
|
+
flex-shrink: 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.backpack-picker-name {
|
|
358
|
+
flex: 1;
|
|
359
|
+
overflow: hidden;
|
|
360
|
+
text-overflow: ellipsis;
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
font-weight: 500;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.backpack-picker-caret {
|
|
366
|
+
opacity: 0.6;
|
|
367
|
+
font-size: 10px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.backpack-picker-dropdown {
|
|
371
|
+
position: absolute;
|
|
372
|
+
top: calc(100% + 4px);
|
|
373
|
+
left: 0;
|
|
374
|
+
right: 0;
|
|
375
|
+
background: var(--bg-surface);
|
|
376
|
+
border: 1px solid var(--border);
|
|
377
|
+
border-radius: 8px;
|
|
378
|
+
box-shadow: 0 4px 16px var(--shadow);
|
|
379
|
+
z-index: 50;
|
|
380
|
+
max-height: 300px;
|
|
381
|
+
overflow-y: auto;
|
|
382
|
+
padding: 4px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.backpack-picker-item {
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: 6px;
|
|
389
|
+
width: 100%;
|
|
390
|
+
padding: 6px 8px;
|
|
391
|
+
background: transparent;
|
|
392
|
+
border: none;
|
|
393
|
+
border-radius: 4px;
|
|
394
|
+
color: var(--fg);
|
|
395
|
+
font-family: inherit;
|
|
396
|
+
font-size: 11px;
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
text-align: left;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.backpack-picker-item:hover {
|
|
402
|
+
background: var(--bg-hover);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.backpack-picker-item.active {
|
|
406
|
+
background: var(--bg-hover);
|
|
407
|
+
font-weight: 600;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.backpack-picker-item-dot {
|
|
411
|
+
display: inline-block;
|
|
412
|
+
width: 8px;
|
|
413
|
+
height: 8px;
|
|
414
|
+
border-radius: 50%;
|
|
415
|
+
background: var(--backpack-color, var(--accent));
|
|
416
|
+
flex-shrink: 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.backpack-picker-item-name {
|
|
420
|
+
flex-shrink: 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.backpack-picker-item-path {
|
|
424
|
+
flex: 1;
|
|
425
|
+
opacity: 0.55;
|
|
426
|
+
overflow: hidden;
|
|
427
|
+
text-overflow: ellipsis;
|
|
428
|
+
white-space: nowrap;
|
|
429
|
+
font-size: 9px;
|
|
430
|
+
text-align: right;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.backpack-picker-divider {
|
|
434
|
+
height: 1px;
|
|
435
|
+
background: var(--border);
|
|
436
|
+
margin: 4px 0;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.backpack-picker-add {
|
|
440
|
+
opacity: 0.75;
|
|
441
|
+
font-style: italic;
|
|
442
|
+
}
|
|
443
|
+
|
|
318
444
|
.sidebar-lock-badge {
|
|
319
445
|
font-size: 10px;
|
|
320
446
|
color: #c08c00;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpack-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Web-based graph visualizer for backpack-ontology — Canvas 2D, force-directed layout, live reload",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Noah Irzinger",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"release:major": "npm version major && git push && git push --tags"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"backpack-ontology": "^0.
|
|
26
|
+
"backpack-ontology": "^0.4.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.5.0",
|