backpack-viewer 0.4.0 → 0.5.1
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/README.md +21 -1
- package/bin/serve.js +133 -3
- package/dist/app/assets/index-B3z5bBGl.css +1 -0
- package/dist/app/assets/index-CKYlU1zT.js +35 -0
- package/dist/app/index.html +2 -2
- package/dist/config.js +1 -0
- package/dist/default-config.json +4 -0
- package/dist/dialog.d.ts +13 -0
- package/dist/dialog.js +116 -0
- package/dist/main.js +13 -2
- package/dist/sidebar.d.ts +3 -2
- package/dist/sidebar.js +28 -8
- package/dist/style.css +98 -2
- package/package.json +2 -2
- package/dist/app/assets/index-DBZCyAjY.js +0 -34
- package/dist/app/assets/index-DlVz8Lz7.css +0 -1
package/dist/config.js
CHANGED
|
@@ -18,6 +18,7 @@ export function loadViewerConfig() {
|
|
|
18
18
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
19
19
|
const user = JSON.parse(raw);
|
|
20
20
|
return {
|
|
21
|
+
server: { ...defaultConfig.server, ...(user.server ?? {}) },
|
|
21
22
|
keybindings: { ...defaultConfig.keybindings, ...(user.keybindings ?? {}) },
|
|
22
23
|
display: { ...defaultConfig.display, ...(user.display ?? {}) },
|
|
23
24
|
layout: { ...defaultConfig.layout, ...(user.layout ?? {}) },
|
package/dist/default-config.json
CHANGED
package/dist/dialog.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
export declare function showConfirm(title: string, message: string): Promise<boolean>;
|
|
4
4
|
/** Show a prompt dialog with an input field. Returns null if cancelled. */
|
|
5
5
|
export declare function showPrompt(title: string, placeholder?: string, defaultValue?: string): Promise<string | null>;
|
|
6
|
+
export interface BackpackAddResult {
|
|
7
|
+
path: string;
|
|
8
|
+
activate: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Show the Add Backpack dialog. Single path field with an optional
|
|
12
|
+
* native folder picker (Chromium only) and drag-and-drop hint. No
|
|
13
|
+
* name field — the display name is derived from the path tail by
|
|
14
|
+
* the backend. No suggestion chips — user pastes any path.
|
|
15
|
+
*
|
|
16
|
+
* Returns null if the user cancels.
|
|
17
|
+
*/
|
|
18
|
+
export declare function showBackpackAddDialog(): Promise<BackpackAddResult | null>;
|
|
6
19
|
/** Show a danger confirmation (for destructive actions). */
|
|
7
20
|
export declare function showDangerConfirm(title: string, message: string): Promise<boolean>;
|
|
8
21
|
/** Show a brief toast notification. */
|
package/dist/dialog.js
CHANGED
|
@@ -87,6 +87,122 @@ export function showPrompt(title, placeholder, defaultValue) {
|
|
|
87
87
|
input.select();
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Show the Add Backpack dialog. Single path field with an optional
|
|
92
|
+
* native folder picker (Chromium only) and drag-and-drop hint. No
|
|
93
|
+
* name field — the display name is derived from the path tail by
|
|
94
|
+
* the backend. No suggestion chips — user pastes any path.
|
|
95
|
+
*
|
|
96
|
+
* Returns null if the user cancels.
|
|
97
|
+
*/
|
|
98
|
+
export function showBackpackAddDialog() {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
const overlay = createOverlay();
|
|
101
|
+
const modal = createModal(overlay, "Add Backpack");
|
|
102
|
+
const description = document.createElement("p");
|
|
103
|
+
description.className = "bp-dialog-message";
|
|
104
|
+
description.textContent =
|
|
105
|
+
"Enter the absolute path to a directory that should become a backpack. It will be shown in the sidebar using the last segment of the path as its display name.";
|
|
106
|
+
modal.appendChild(description);
|
|
107
|
+
const pathLabel = document.createElement("label");
|
|
108
|
+
pathLabel.className = "bp-dialog-label";
|
|
109
|
+
pathLabel.textContent = "Path";
|
|
110
|
+
modal.appendChild(pathLabel);
|
|
111
|
+
const pathRow = document.createElement("div");
|
|
112
|
+
pathRow.className = "bp-dialog-path-row";
|
|
113
|
+
modal.appendChild(pathRow);
|
|
114
|
+
const pathInput = document.createElement("input");
|
|
115
|
+
pathInput.type = "text";
|
|
116
|
+
pathInput.className = "bp-dialog-input bp-dialog-path-input";
|
|
117
|
+
pathInput.placeholder = "/Users/you/OneDrive/work";
|
|
118
|
+
pathRow.appendChild(pathInput);
|
|
119
|
+
const browseBtn = document.createElement("button");
|
|
120
|
+
browseBtn.type = "button";
|
|
121
|
+
browseBtn.className = "bp-dialog-btn bp-dialog-browse-btn";
|
|
122
|
+
browseBtn.textContent = "Browse...";
|
|
123
|
+
pathRow.appendChild(browseBtn);
|
|
124
|
+
// File System Access API is Chromium-only. On Safari/Firefox the
|
|
125
|
+
// picker doesn't exist; even on Chromium it returns a handle, not
|
|
126
|
+
// a filesystem path, so the user still types or pastes the
|
|
127
|
+
// absolute path. The picker is only a hint.
|
|
128
|
+
const hasNativePicker = typeof window.showDirectoryPicker === "function";
|
|
129
|
+
if (!hasNativePicker) {
|
|
130
|
+
browseBtn.disabled = true;
|
|
131
|
+
browseBtn.title =
|
|
132
|
+
"Browser doesn't support native folder picker — paste the path manually";
|
|
133
|
+
}
|
|
134
|
+
browseBtn.addEventListener("click", async (e) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
try {
|
|
137
|
+
const handle = await window.showDirectoryPicker({
|
|
138
|
+
mode: "read",
|
|
139
|
+
});
|
|
140
|
+
pickerHint.textContent = `Picked "${handle.name}" — paste the absolute path to it below.`;
|
|
141
|
+
pathInput.focus();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// User cancelled
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const pickerHint = document.createElement("div");
|
|
148
|
+
pickerHint.className = "bp-dialog-picker-hint";
|
|
149
|
+
modal.appendChild(pickerHint);
|
|
150
|
+
// Activate checkbox
|
|
151
|
+
const activateRow = document.createElement("div");
|
|
152
|
+
activateRow.className = "bp-dialog-activate-row";
|
|
153
|
+
const activateCheckbox = document.createElement("input");
|
|
154
|
+
activateCheckbox.type = "checkbox";
|
|
155
|
+
activateCheckbox.id = "bp-dialog-activate";
|
|
156
|
+
activateCheckbox.checked = true;
|
|
157
|
+
const activateLabel = document.createElement("label");
|
|
158
|
+
activateLabel.htmlFor = "bp-dialog-activate";
|
|
159
|
+
activateLabel.textContent = "Switch to this backpack after registering";
|
|
160
|
+
activateRow.appendChild(activateCheckbox);
|
|
161
|
+
activateRow.appendChild(activateLabel);
|
|
162
|
+
modal.appendChild(activateRow);
|
|
163
|
+
// Drag-and-drop: dropping a folder gives us the folder name via
|
|
164
|
+
// webkitGetAsEntry but not the full OS path (sandboxed). Use it as
|
|
165
|
+
// a visual hint only.
|
|
166
|
+
pathInput.addEventListener("dragover", (e) => {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
pathInput.classList.add("bp-dialog-drag-over");
|
|
169
|
+
});
|
|
170
|
+
pathInput.addEventListener("dragleave", () => {
|
|
171
|
+
pathInput.classList.remove("bp-dialog-drag-over");
|
|
172
|
+
});
|
|
173
|
+
pathInput.addEventListener("drop", (e) => {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
pathInput.classList.remove("bp-dialog-drag-over");
|
|
176
|
+
const items = e.dataTransfer?.items;
|
|
177
|
+
if (!items || items.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
const entry = items[0].webkitGetAsEntry?.();
|
|
180
|
+
if (entry?.isDirectory) {
|
|
181
|
+
pickerHint.textContent = `Dropped "${entry.name}" — paste the absolute path to it below.`;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const submit = () => {
|
|
185
|
+
const p = pathInput.value.trim();
|
|
186
|
+
if (!p)
|
|
187
|
+
return;
|
|
188
|
+
overlay.remove();
|
|
189
|
+
resolve({ path: p, activate: activateCheckbox.checked });
|
|
190
|
+
};
|
|
191
|
+
pathInput.addEventListener("keydown", (e) => {
|
|
192
|
+
if (e.key === "Enter")
|
|
193
|
+
submit();
|
|
194
|
+
if (e.key === "Escape") {
|
|
195
|
+
overlay.remove();
|
|
196
|
+
resolve(null);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
addButtons(modal, [
|
|
200
|
+
{ label: "Cancel", onClick: () => { overlay.remove(); resolve(null); } },
|
|
201
|
+
{ label: "Register", accent: true, onClick: submit },
|
|
202
|
+
]);
|
|
203
|
+
pathInput.focus();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
90
206
|
/** Show a danger confirmation (for destructive actions). */
|
|
91
207
|
export function showDangerConfirm(title, message) {
|
|
92
208
|
return new Promise((resolve) => {
|
package/dist/main.js
CHANGED
|
@@ -528,11 +528,11 @@ async function main() {
|
|
|
528
528
|
// live-reload channel, so we refresh immediately as fallback.
|
|
529
529
|
await refreshBackpacksAndGraphs();
|
|
530
530
|
},
|
|
531
|
-
onBackpackRegister: async (
|
|
531
|
+
onBackpackRegister: async (p, activate) => {
|
|
532
532
|
await fetch("/api/backpacks", {
|
|
533
533
|
method: "POST",
|
|
534
534
|
headers: { "Content-Type": "application/json" },
|
|
535
|
-
body: JSON.stringify({
|
|
535
|
+
body: JSON.stringify({ path: p, activate }),
|
|
536
536
|
});
|
|
537
537
|
await refreshBackpacksAndGraphs();
|
|
538
538
|
},
|
|
@@ -771,6 +771,17 @@ async function main() {
|
|
|
771
771
|
sidebar.setBackpacks(list);
|
|
772
772
|
}
|
|
773
773
|
catch { }
|
|
774
|
+
// Fire-and-forget stale-version check. If we're running an out-of-date
|
|
775
|
+
// viewer (classic npx cache trap), show a banner in the sidebar with
|
|
776
|
+
// the exact command to unblock.
|
|
777
|
+
fetch("/api/version-check")
|
|
778
|
+
.then((r) => r.json())
|
|
779
|
+
.then((info) => {
|
|
780
|
+
if (info.stale && info.latest) {
|
|
781
|
+
sidebar.setStaleVersionBanner(info.current, info.latest);
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
.catch(() => { });
|
|
774
785
|
// Load ontology list (local + remote in parallel)
|
|
775
786
|
const [summaries, remotes] = await Promise.all([
|
|
776
787
|
listOntologies(),
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -14,10 +14,11 @@ export interface SidebarCallbacks {
|
|
|
14
14
|
onBranchDelete?: (graphName: string, branchName: string) => void;
|
|
15
15
|
onSnippetLoad?: (graphName: string, snippetId: string) => void;
|
|
16
16
|
onSnippetDelete?: (graphName: string, snippetId: string) => void;
|
|
17
|
-
onBackpackSwitch?: (
|
|
18
|
-
onBackpackRegister?: (
|
|
17
|
+
onBackpackSwitch?: (pathOrName: string) => void;
|
|
18
|
+
onBackpackRegister?: (path: string, activate: boolean) => void;
|
|
19
19
|
}
|
|
20
20
|
export declare function initSidebar(container: HTMLElement, onSelectOrCallbacks: ((name: string) => void) | SidebarCallbacks): {
|
|
21
|
+
setStaleVersionBanner(current: string, latest: string): void;
|
|
21
22
|
setBackpacks(list: BackpackSummary[]): void;
|
|
22
23
|
setActiveBackpack(entry: BackpackSummary): void;
|
|
23
24
|
getActiveBackpack(): BackpackSummary | null;
|
package/dist/sidebar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { showConfirm, showPrompt } from "./dialog";
|
|
1
|
+
import { showConfirm, showPrompt, showBackpackAddDialog } from "./dialog";
|
|
2
2
|
function formatTokenCount(n) {
|
|
3
3
|
if (n >= 1000)
|
|
4
4
|
return `${(n / 1000).toFixed(1)}k tokens`;
|
|
@@ -51,6 +51,14 @@ export function initSidebar(container, onSelectOrCallbacks) {
|
|
|
51
51
|
headingRow.appendChild(heading);
|
|
52
52
|
headingRow.appendChild(collapseBtn);
|
|
53
53
|
container.appendChild(headingRow);
|
|
54
|
+
// Stale-version banner — hidden by default. setStaleVersionBanner()
|
|
55
|
+
// populates and reveals it when the startup check detects that the
|
|
56
|
+
// running viewer is older than the latest published npm version
|
|
57
|
+
// (classic npx cache trap).
|
|
58
|
+
const staleBanner = document.createElement("div");
|
|
59
|
+
staleBanner.className = "sidebar-stale-banner";
|
|
60
|
+
staleBanner.hidden = true;
|
|
61
|
+
container.appendChild(staleBanner);
|
|
54
62
|
// Backpack picker pill — discrete indicator of the active backpack with
|
|
55
63
|
// a dropdown to switch between registered ones.
|
|
56
64
|
const backpackPicker = document.createElement("button");
|
|
@@ -146,14 +154,10 @@ export function initSidebar(container, onSelectOrCallbacks) {
|
|
|
146
154
|
closePicker();
|
|
147
155
|
if (!cbs.onBackpackRegister)
|
|
148
156
|
return;
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
157
|
+
const result = await showBackpackAddDialog();
|
|
158
|
+
if (!result)
|
|
151
159
|
return;
|
|
152
|
-
|
|
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);
|
|
160
|
+
cbs.onBackpackRegister(result.path, result.activate);
|
|
157
161
|
});
|
|
158
162
|
pickerDropdown.appendChild(addItem);
|
|
159
163
|
}
|
|
@@ -185,6 +189,22 @@ export function initSidebar(container, onSelectOrCallbacks) {
|
|
|
185
189
|
}
|
|
186
190
|
});
|
|
187
191
|
return {
|
|
192
|
+
setStaleVersionBanner(current, latest) {
|
|
193
|
+
staleBanner.replaceChildren();
|
|
194
|
+
const title = document.createElement("div");
|
|
195
|
+
title.className = "sidebar-stale-banner-title";
|
|
196
|
+
title.textContent = `Viewer ${current} is out of date`;
|
|
197
|
+
const subtitle = document.createElement("div");
|
|
198
|
+
subtitle.className = "sidebar-stale-banner-subtitle";
|
|
199
|
+
subtitle.textContent = `Latest is ${latest}. Your version is stuck because of an npx cache.`;
|
|
200
|
+
const hint = document.createElement("pre");
|
|
201
|
+
hint.className = "sidebar-stale-banner-hint";
|
|
202
|
+
hint.textContent = "npm cache clean --force\nnpx backpack-viewer@latest";
|
|
203
|
+
staleBanner.appendChild(title);
|
|
204
|
+
staleBanner.appendChild(subtitle);
|
|
205
|
+
staleBanner.appendChild(hint);
|
|
206
|
+
staleBanner.hidden = false;
|
|
207
|
+
},
|
|
188
208
|
setBackpacks(list) {
|
|
189
209
|
currentBackpacks = list.slice();
|
|
190
210
|
const active = list.find((b) => b.active) ?? null;
|
package/dist/style.css
CHANGED
|
@@ -318,6 +318,40 @@ body {
|
|
|
318
318
|
opacity: 1;
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/* --- Stale viewer version banner --- */
|
|
322
|
+
|
|
323
|
+
.sidebar-stale-banner {
|
|
324
|
+
background: #fff3cd;
|
|
325
|
+
color: #5c3a00;
|
|
326
|
+
border: 1px solid #e6c263;
|
|
327
|
+
border-radius: 6px;
|
|
328
|
+
padding: 10px 12px;
|
|
329
|
+
margin-bottom: 10px;
|
|
330
|
+
font-size: 11px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.sidebar-stale-banner-title {
|
|
334
|
+
font-weight: 600;
|
|
335
|
+
margin-bottom: 2px;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.sidebar-stale-banner-subtitle {
|
|
339
|
+
opacity: 0.85;
|
|
340
|
+
margin-bottom: 6px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.sidebar-stale-banner-hint {
|
|
344
|
+
background: rgba(0, 0, 0, 0.08);
|
|
345
|
+
border-radius: 4px;
|
|
346
|
+
padding: 6px 8px;
|
|
347
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
348
|
+
font-size: 10px;
|
|
349
|
+
line-height: 1.4;
|
|
350
|
+
margin: 0;
|
|
351
|
+
white-space: pre-wrap;
|
|
352
|
+
word-break: break-all;
|
|
353
|
+
}
|
|
354
|
+
|
|
321
355
|
/* --- Backpack picker (active backpack indicator + switcher) --- */
|
|
322
356
|
|
|
323
357
|
.backpack-picker-container {
|
|
@@ -330,10 +364,10 @@ body {
|
|
|
330
364
|
align-items: center;
|
|
331
365
|
gap: 6px;
|
|
332
366
|
width: 100%;
|
|
333
|
-
padding:
|
|
367
|
+
padding: 6px 10px;
|
|
334
368
|
background: var(--bg-base);
|
|
335
369
|
border: 1px solid var(--border);
|
|
336
|
-
border-radius:
|
|
370
|
+
border-radius: 6px;
|
|
337
371
|
color: var(--fg);
|
|
338
372
|
font-size: 11px;
|
|
339
373
|
font-family: inherit;
|
|
@@ -2135,6 +2169,68 @@ body {
|
|
|
2135
2169
|
border-color: var(--accent);
|
|
2136
2170
|
}
|
|
2137
2171
|
|
|
2172
|
+
.bp-dialog-label {
|
|
2173
|
+
display: block;
|
|
2174
|
+
font-size: 11px;
|
|
2175
|
+
color: var(--text-muted);
|
|
2176
|
+
margin-bottom: 4px;
|
|
2177
|
+
margin-top: 8px;
|
|
2178
|
+
text-transform: uppercase;
|
|
2179
|
+
letter-spacing: 0.04em;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
.bp-dialog-path-row {
|
|
2183
|
+
display: flex;
|
|
2184
|
+
gap: 8px;
|
|
2185
|
+
margin-bottom: 4px;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
.bp-dialog-path-input {
|
|
2189
|
+
flex: 1;
|
|
2190
|
+
margin-bottom: 0;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
.bp-dialog-browse-btn {
|
|
2194
|
+
flex-shrink: 0;
|
|
2195
|
+
padding: 8px 14px;
|
|
2196
|
+
font-size: 12px;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
.bp-dialog-browse-btn:disabled {
|
|
2200
|
+
opacity: 0.4;
|
|
2201
|
+
cursor: not-allowed;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
.bp-dialog-path-input.bp-dialog-drag-over {
|
|
2205
|
+
border-color: var(--accent);
|
|
2206
|
+
background: var(--bg-hover);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
.bp-dialog-picker-hint {
|
|
2210
|
+
font-size: 11px;
|
|
2211
|
+
color: var(--text-muted);
|
|
2212
|
+
min-height: 1em;
|
|
2213
|
+
margin-bottom: 8px;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
.bp-dialog-activate-row {
|
|
2217
|
+
display: flex;
|
|
2218
|
+
align-items: center;
|
|
2219
|
+
gap: 8px;
|
|
2220
|
+
margin-top: 8px;
|
|
2221
|
+
margin-bottom: 12px;
|
|
2222
|
+
font-size: 12px;
|
|
2223
|
+
color: var(--text-muted);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
.bp-dialog-activate-row input[type="checkbox"] {
|
|
2227
|
+
margin: 0;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
.bp-dialog-activate-row label {
|
|
2231
|
+
cursor: pointer;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2138
2234
|
.bp-dialog-buttons {
|
|
2139
2235
|
display: flex;
|
|
2140
2236
|
justify-content: flex-end;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backpack-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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.5.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.5.0",
|