backpack-viewer 0.6.0 → 0.7.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/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-Lvl7EMM_.js +6 -0
- package/dist/app/index.html +2 -2
- package/dist/bridge.d.ts +22 -0
- package/dist/bridge.js +41 -0
- 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 +4 -0
- 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/share/backpack-extension.json +20 -0
- package/dist/extensions/share/src/index.js +357 -0
- package/dist/extensions/share/style.css +151 -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/main.js +189 -29
- package/dist/search.js +1 -1
- package/dist/server-api-routes.d.ts +56 -0
- package/dist/server-api-routes.js +460 -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/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-BROJmzot.js +0 -35
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny DOM helpers used across the viewer to avoid `innerHTML` for
|
|
3
|
+
* static SVG icons and other markup that previously got built as
|
|
4
|
+
* strings. Keeps every call site CSP-clean and XSS-safe by construction
|
|
5
|
+
* (no string concatenation, no parsing of user content).
|
|
6
|
+
*/
|
|
7
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
8
|
+
/**
|
|
9
|
+
* Build an SVG icon from a list of child element specs. Each spec is
|
|
10
|
+
* `{ tag, attrs }` where tag is one of the standard SVG element names.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* makeSvgIcon({ size: 14 }, [
|
|
14
|
+
* { tag: "polyline", attrs: { points: "11 17 6 12 11 7" } },
|
|
15
|
+
* { tag: "polyline", attrs: { points: "18 17 13 12 18 7" } },
|
|
16
|
+
* ])
|
|
17
|
+
*/
|
|
18
|
+
export function makeSvgIcon(opts, children) {
|
|
19
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
20
|
+
const size = opts.size ?? 16;
|
|
21
|
+
svg.setAttribute("width", String(size));
|
|
22
|
+
svg.setAttribute("height", String(size));
|
|
23
|
+
svg.setAttribute("viewBox", opts.viewBox ?? "0 0 24 24");
|
|
24
|
+
svg.setAttribute("fill", "none");
|
|
25
|
+
svg.setAttribute("stroke", "currentColor");
|
|
26
|
+
svg.setAttribute("stroke-width", String(opts.strokeWidth ?? 2));
|
|
27
|
+
if (opts.strokeLinecap)
|
|
28
|
+
svg.setAttribute("stroke-linecap", opts.strokeLinecap);
|
|
29
|
+
if (opts.strokeLinejoin)
|
|
30
|
+
svg.setAttribute("stroke-linejoin", opts.strokeLinejoin);
|
|
31
|
+
if (opts.className)
|
|
32
|
+
svg.setAttribute("class", opts.className);
|
|
33
|
+
for (const child of children) {
|
|
34
|
+
const el = document.createElementNS(SVG_NS, child.tag);
|
|
35
|
+
for (const [k, v] of Object.entries(child.attrs)) {
|
|
36
|
+
el.setAttribute(k, String(v));
|
|
37
|
+
}
|
|
38
|
+
svg.appendChild(el);
|
|
39
|
+
}
|
|
40
|
+
return svg;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot the current children of an element so they can be restored
|
|
44
|
+
* later via `restoreChildren()`. Used by inline-edit flows that
|
|
45
|
+
* temporarily replace a row's contents with an input.
|
|
46
|
+
*
|
|
47
|
+
* Returns a frozen array of cloned nodes — the original references are
|
|
48
|
+
* NOT preserved (which would break if the parent is mutated). Cloning
|
|
49
|
+
* is fine because the snapshotted markup is static — no event handlers
|
|
50
|
+
* to lose.
|
|
51
|
+
*/
|
|
52
|
+
export function snapshotChildren(el) {
|
|
53
|
+
return Array.from(el.childNodes).map((n) => n.cloneNode(true));
|
|
54
|
+
}
|
|
55
|
+
export function restoreChildren(el, snapshot) {
|
|
56
|
+
el.replaceChildren(...snapshot);
|
|
57
|
+
}
|
package/dist/empty-state.js
CHANGED
|
@@ -1,37 +1,69 @@
|
|
|
1
|
+
import { makeSvgIcon } from "./dom-utils";
|
|
1
2
|
export function initEmptyState(container) {
|
|
2
3
|
const el = document.createElement("div");
|
|
3
4
|
el.className = "empty-state";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
5
|
+
// Background circles + decorative line art
|
|
6
|
+
const bg = document.createElement("div");
|
|
7
|
+
bg.className = "empty-state-bg";
|
|
8
|
+
for (const cls of ["c1", "c2", "c3", "c4", "c5"]) {
|
|
9
|
+
const circle = document.createElement("div");
|
|
10
|
+
circle.className = `empty-state-circle ${cls}`;
|
|
11
|
+
bg.appendChild(circle);
|
|
12
|
+
}
|
|
13
|
+
bg.appendChild(makeSvgIcon({ size: 0, viewBox: "0 0 400 300", className: "empty-state-lines" }, [
|
|
14
|
+
{ tag: "line", attrs: { x1: 80, y1: 60, x2: 220, y2: 140, "stroke-width": "0.5", opacity: "0.15" } },
|
|
15
|
+
{ tag: "line", attrs: { x1: 220, y1: 140, x2: 320, y2: 80, "stroke-width": "0.5", opacity: "0.15" } },
|
|
16
|
+
{ tag: "line", attrs: { x1: 220, y1: 140, x2: 160, y2: 240, "stroke-width": "0.5", opacity: "0.15" } },
|
|
17
|
+
{ tag: "line", attrs: { x1: 160, y1: 240, x2: 300, y2: 220, "stroke-width": "0.5", opacity: "0.15" } },
|
|
18
|
+
]));
|
|
19
|
+
// makeSvgIcon sets width/height attrs; the lines layer is sized by CSS
|
|
20
|
+
// (preserveAspectRatio comes from the empty-state-lines class).
|
|
21
|
+
const linesSvg = bg.querySelector(".empty-state-lines");
|
|
22
|
+
if (linesSvg) {
|
|
23
|
+
linesSvg.removeAttribute("width");
|
|
24
|
+
linesSvg.removeAttribute("height");
|
|
25
|
+
linesSvg.setAttribute("preserveAspectRatio", "xMidYMid slice");
|
|
26
|
+
}
|
|
27
|
+
el.appendChild(bg);
|
|
28
|
+
// Content card
|
|
29
|
+
const content = document.createElement("div");
|
|
30
|
+
content.className = "empty-state-content";
|
|
31
|
+
const iconWrap = document.createElement("div");
|
|
32
|
+
iconWrap.className = "empty-state-icon";
|
|
33
|
+
iconWrap.appendChild(makeSvgIcon({ size: 48, strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }, [
|
|
34
|
+
{ tag: "path", attrs: { d: "M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 002 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0022 16z" } },
|
|
35
|
+
{ tag: "polyline", attrs: { points: "3.27 6.96 12 12.01 20.73 6.96" } },
|
|
36
|
+
{ tag: "line", attrs: { x1: 12, y1: 22.08, x2: 12, y2: 12 } },
|
|
37
|
+
]));
|
|
38
|
+
content.appendChild(iconWrap);
|
|
39
|
+
const title = document.createElement("h2");
|
|
40
|
+
title.className = "empty-state-title";
|
|
41
|
+
title.textContent = "No learning graphs yet";
|
|
42
|
+
content.appendChild(title);
|
|
43
|
+
const desc = document.createElement("p");
|
|
44
|
+
desc.className = "empty-state-desc";
|
|
45
|
+
desc.textContent =
|
|
46
|
+
"Connect Backpack to Claude, then start a conversation. Claude will build your first learning graph automatically.";
|
|
47
|
+
content.appendChild(desc);
|
|
48
|
+
const setup = document.createElement("div");
|
|
49
|
+
setup.className = "empty-state-setup";
|
|
50
|
+
const setupLabel = document.createElement("div");
|
|
51
|
+
setupLabel.className = "empty-state-label";
|
|
52
|
+
setupLabel.textContent = "Add Backpack to Claude Code:";
|
|
53
|
+
const setupCode = document.createElement("code");
|
|
54
|
+
setupCode.className = "empty-state-code";
|
|
55
|
+
setupCode.textContent = "claude mcp add backpack-local -s user -- npx backpack-ontology@latest";
|
|
56
|
+
setup.append(setupLabel, setupCode);
|
|
57
|
+
content.appendChild(setup);
|
|
58
|
+
const hint = document.createElement("p");
|
|
59
|
+
hint.className = "empty-state-hint";
|
|
60
|
+
hint.append("Press ");
|
|
61
|
+
const kbd = document.createElement("kbd");
|
|
62
|
+
kbd.textContent = "?";
|
|
63
|
+
hint.appendChild(kbd);
|
|
64
|
+
hint.append(" for keyboard shortcuts");
|
|
65
|
+
content.appendChild(hint);
|
|
66
|
+
el.appendChild(content);
|
|
35
67
|
container.appendChild(el);
|
|
36
68
|
return {
|
|
37
69
|
show() { el.classList.remove("hidden"); },
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ViewerExtensionAPI, ViewerHost } from "./types";
|
|
2
|
+
import type { Taskbar } from "./taskbar";
|
|
3
|
+
import type { PanelMount } from "./panel-mount";
|
|
4
|
+
/**
|
|
5
|
+
* Construct a per-extension `ViewerExtensionAPI` instance. Each loaded
|
|
6
|
+
* extension gets its own API object whose closures know its name — that
|
|
7
|
+
* scoping is what makes per-extension settings + per-extension fetch
|
|
8
|
+
* proxies work.
|
|
9
|
+
*
|
|
10
|
+
* The API surface is intentionally minimal in v1: graph reads, graph
|
|
11
|
+
* mutations (auto-undo, auto-persist, auto-rerender), viewer driving,
|
|
12
|
+
* mount surfaces, settings, network proxy. Anything else extensions
|
|
13
|
+
* need will get added in v2 with a viewerApi version bump.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createExtensionAPI(extensionName: string, host: ViewerHost, taskbar: Taskbar, panelMount: PanelMount): ViewerExtensionAPI;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { VIEWER_API_VERSION } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Construct a per-extension `ViewerExtensionAPI` instance. Each loaded
|
|
4
|
+
* extension gets its own API object whose closures know its name — that
|
|
5
|
+
* scoping is what makes per-extension settings + per-extension fetch
|
|
6
|
+
* proxies work.
|
|
7
|
+
*
|
|
8
|
+
* The API surface is intentionally minimal in v1: graph reads, graph
|
|
9
|
+
* mutations (auto-undo, auto-persist, auto-rerender), viewer driving,
|
|
10
|
+
* mount surfaces, settings, network proxy. Anything else extensions
|
|
11
|
+
* need will get added in v2 with a viewerApi version bump.
|
|
12
|
+
*/
|
|
13
|
+
export function createExtensionAPI(extensionName, host, taskbar, panelMount) {
|
|
14
|
+
function newId() {
|
|
15
|
+
// Sufficient for client-side ids; backpack-ontology accepts arbitrary
|
|
16
|
+
// string ids and we already use a similar pattern in main.ts callbacks.
|
|
17
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
|
|
18
|
+
}
|
|
19
|
+
function ensureGraph() {
|
|
20
|
+
const data = host.getGraph();
|
|
21
|
+
if (!data)
|
|
22
|
+
throw new Error("no graph loaded in viewer");
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
name: extensionName,
|
|
27
|
+
viewerApiVersion: VIEWER_API_VERSION,
|
|
28
|
+
// --- Graph reads ---
|
|
29
|
+
getGraph: () => host.getGraph(),
|
|
30
|
+
getGraphName: () => host.getGraphName(),
|
|
31
|
+
getSelection: () => host.getSelection(),
|
|
32
|
+
getFocus: () => host.getFocus(),
|
|
33
|
+
// --- Events ---
|
|
34
|
+
on(event, callback) {
|
|
35
|
+
return host.subscribe(event, callback);
|
|
36
|
+
},
|
|
37
|
+
// --- Graph mutations ---
|
|
38
|
+
async addNode(type, properties) {
|
|
39
|
+
if (!type)
|
|
40
|
+
throw new Error("addNode: type is required");
|
|
41
|
+
const data = ensureGraph();
|
|
42
|
+
host.snapshotForUndo();
|
|
43
|
+
const id = newId();
|
|
44
|
+
const now = new Date().toISOString();
|
|
45
|
+
data.nodes.push({
|
|
46
|
+
id,
|
|
47
|
+
type,
|
|
48
|
+
properties: properties,
|
|
49
|
+
createdAt: now,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
});
|
|
52
|
+
await host.saveCurrentGraph();
|
|
53
|
+
return id;
|
|
54
|
+
},
|
|
55
|
+
async updateNode(nodeId, properties) {
|
|
56
|
+
const data = ensureGraph();
|
|
57
|
+
const node = data.nodes.find((n) => n.id === nodeId);
|
|
58
|
+
if (!node)
|
|
59
|
+
throw new Error(`updateNode: node not found: ${nodeId}`);
|
|
60
|
+
host.snapshotForUndo();
|
|
61
|
+
node.properties = { ...node.properties, ...properties };
|
|
62
|
+
node.updatedAt = new Date().toISOString();
|
|
63
|
+
await host.saveCurrentGraph();
|
|
64
|
+
},
|
|
65
|
+
async removeNode(nodeId) {
|
|
66
|
+
const data = ensureGraph();
|
|
67
|
+
const node = data.nodes.find((n) => n.id === nodeId);
|
|
68
|
+
if (!node)
|
|
69
|
+
throw new Error(`removeNode: node not found: ${nodeId}`);
|
|
70
|
+
host.snapshotForUndo();
|
|
71
|
+
data.nodes = data.nodes.filter((n) => n.id !== nodeId);
|
|
72
|
+
data.edges = data.edges.filter((e) => e.sourceId !== nodeId && e.targetId !== nodeId);
|
|
73
|
+
await host.saveCurrentGraph();
|
|
74
|
+
},
|
|
75
|
+
async addEdge(sourceId, targetId, type) {
|
|
76
|
+
if (!sourceId || !targetId || !type) {
|
|
77
|
+
throw new Error("addEdge: sourceId, targetId, and type are required");
|
|
78
|
+
}
|
|
79
|
+
const data = ensureGraph();
|
|
80
|
+
if (!data.nodes.find((n) => n.id === sourceId)) {
|
|
81
|
+
throw new Error(`addEdge: source not found: ${sourceId}`);
|
|
82
|
+
}
|
|
83
|
+
if (!data.nodes.find((n) => n.id === targetId)) {
|
|
84
|
+
throw new Error(`addEdge: target not found: ${targetId}`);
|
|
85
|
+
}
|
|
86
|
+
host.snapshotForUndo();
|
|
87
|
+
const id = newId();
|
|
88
|
+
data.edges.push({ id, sourceId, targetId, type });
|
|
89
|
+
await host.saveCurrentGraph();
|
|
90
|
+
return id;
|
|
91
|
+
},
|
|
92
|
+
async removeEdge(edgeId) {
|
|
93
|
+
const data = ensureGraph();
|
|
94
|
+
const edge = data.edges.find((e) => e.id === edgeId);
|
|
95
|
+
if (!edge)
|
|
96
|
+
throw new Error(`removeEdge: edge not found: ${edgeId}`);
|
|
97
|
+
host.snapshotForUndo();
|
|
98
|
+
data.edges = data.edges.filter((e) => e.id !== edgeId);
|
|
99
|
+
await host.saveCurrentGraph();
|
|
100
|
+
},
|
|
101
|
+
// --- Viewer driving ---
|
|
102
|
+
panToNode: (nodeId) => host.panToNode(nodeId),
|
|
103
|
+
focusNodes: (nodeIds, hops) => host.focusNodes(nodeIds, hops),
|
|
104
|
+
exitFocus: () => host.exitFocus(),
|
|
105
|
+
// --- UI mounting ---
|
|
106
|
+
registerTaskbarIcon(opts) {
|
|
107
|
+
return taskbar.register(opts);
|
|
108
|
+
},
|
|
109
|
+
mountPanel(element, opts) {
|
|
110
|
+
return panelMount.mount(extensionName, element, opts);
|
|
111
|
+
},
|
|
112
|
+
// --- Settings ---
|
|
113
|
+
settings: {
|
|
114
|
+
async get(key) {
|
|
115
|
+
const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings`;
|
|
116
|
+
const res = await fetch(url);
|
|
117
|
+
if (!res.ok)
|
|
118
|
+
return null;
|
|
119
|
+
const all = (await res.json());
|
|
120
|
+
return key in all ? all[key] : null;
|
|
121
|
+
},
|
|
122
|
+
async set(key, value) {
|
|
123
|
+
const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings/${encodeURIComponent(key)}`;
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
method: "PUT",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ value }),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const err = await res.text().catch(() => "");
|
|
131
|
+
throw new Error(`settings.set failed: ${err || res.status}`);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
async remove(key) {
|
|
135
|
+
const url = `/api/extensions/${encodeURIComponent(extensionName)}/settings/${encodeURIComponent(key)}`;
|
|
136
|
+
const res = await fetch(url, { method: "DELETE" });
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const err = await res.text().catch(() => "");
|
|
139
|
+
throw new Error(`settings.remove failed: ${err || res.status}`);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
// --- Network ---
|
|
144
|
+
async fetch(url, init = {}) {
|
|
145
|
+
// The browser-side viewer.fetch wraps the call into a POST against
|
|
146
|
+
// the per-extension proxy endpoint. The proxy validates the URL
|
|
147
|
+
// against the manifest's network allowlist and injects any
|
|
148
|
+
// configured headers (env-var or literal) server-side.
|
|
149
|
+
const proxyUrl = `/api/extensions/${encodeURIComponent(extensionName)}/fetch`;
|
|
150
|
+
const headers = {};
|
|
151
|
+
if (init.headers) {
|
|
152
|
+
if (init.headers instanceof Headers) {
|
|
153
|
+
init.headers.forEach((v, k) => {
|
|
154
|
+
headers[k] = v;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else if (Array.isArray(init.headers)) {
|
|
158
|
+
for (const [k, v] of init.headers)
|
|
159
|
+
headers[k] = v;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
Object.assign(headers, init.headers);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const body = typeof init.body === "string" || init.body == null
|
|
166
|
+
? init.body
|
|
167
|
+
: // RequestInit body can be many things; for v1 we only support
|
|
168
|
+
// strings (which is what JSON-based APIs use). Throw on
|
|
169
|
+
// anything else so extension authors get a clear error.
|
|
170
|
+
(() => {
|
|
171
|
+
throw new Error("viewer.fetch only supports string bodies in v1");
|
|
172
|
+
})();
|
|
173
|
+
return fetch(proxyUrl, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { "Content-Type": "application/json" },
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
url,
|
|
178
|
+
method: init.method ?? "POST",
|
|
179
|
+
headers,
|
|
180
|
+
body,
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"viewerApi": "1",
|
|
5
|
+
"displayName": "Chat",
|
|
6
|
+
"description": "Chat with Claude (or another LLM provider) about your graph. The model can read nodes, follow edges, and add/edit/remove graph data with your approval.",
|
|
7
|
+
"entry": "src/index.js",
|
|
8
|
+
"stylesheet": "style.css",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"graph": ["read", "write"],
|
|
11
|
+
"viewer": ["focus", "pan"],
|
|
12
|
+
"settings": true,
|
|
13
|
+
"network": [
|
|
14
|
+
{
|
|
15
|
+
"origin": "https://api.anthropic.com",
|
|
16
|
+
"injectHeaders": {
|
|
17
|
+
"x-api-key": { "fromEnv": "ANTHROPIC_API_KEY" },
|
|
18
|
+
"anthropic-version": { "literal": "2023-06-01" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createAnthropicProvider } from "./providers/anthropic.js";
|
|
2
|
+
import { createChatPanelController } from "./panel.js";
|
|
3
|
+
/**
|
|
4
|
+
* Chat extension entry. The viewer's loader dynamic-imports this file
|
|
5
|
+
* and calls `activate(viewer)`.
|
|
6
|
+
*
|
|
7
|
+
* Wires up:
|
|
8
|
+
* - One LLM provider (Anthropic for v1; future: OpenAI, Ollama, …)
|
|
9
|
+
* - The chat panel controller (mounts/unmounts the panel)
|
|
10
|
+
* - A taskbar icon that toggles the panel
|
|
11
|
+
*
|
|
12
|
+
* Everything goes through the viewer extension API. There is no
|
|
13
|
+
* special-case wiring into viewer internals — this extension uses
|
|
14
|
+
* exactly the same surface a third-party extension would.
|
|
15
|
+
*/
|
|
16
|
+
export function activate(viewer) {
|
|
17
|
+
const provider = createAnthropicProvider(viewer.fetch.bind(viewer));
|
|
18
|
+
const controller = createChatPanelController(viewer, provider);
|
|
19
|
+
viewer.registerTaskbarIcon({
|
|
20
|
+
label: "Chat",
|
|
21
|
+
// Top-right groups the chat toggle with the existing top-bar
|
|
22
|
+
// controls (zoom, copy-prompt, theme) and aligns visually with the
|
|
23
|
+
// chat panel itself, which docks to the right side of the canvas.
|
|
24
|
+
position: "top-right",
|
|
25
|
+
onClick: () => controller.toggle(),
|
|
26
|
+
});
|
|
27
|
+
// Restore any persisted history before the user opens the panel.
|
|
28
|
+
// Errors are non-fatal — first run has nothing to restore.
|
|
29
|
+
controller.loadHistory().catch((err) => {
|
|
30
|
+
console.warn("[chat] failed to load history:", err);
|
|
31
|
+
});
|
|
32
|
+
}
|