@andespindola/brainlink 0.1.0-beta.153 → 0.1.0-beta.155
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/AGENTS.md +2 -2
- package/README.md +23 -7
- package/dist/application/add-note.js +12 -3
- package/dist/application/canonical-context-links.js +209 -0
- package/dist/application/frontend/client-css.js +77 -0
- package/dist/application/frontend/client-html.js +4 -0
- package/dist/application/frontend/client-js.js +490 -16
- package/dist/application/frontend/client-render-worker-js.js +53 -0
- package/dist/application/get-graph-layout.js +3 -2
- package/dist/application/graph-view-state.js +66 -0
- package/dist/application/search-graph-node-ids.js +14 -5
- package/dist/application/server/routes.js +46 -1
- package/dist/cli/commands/write-commands.js +47 -8
- package/dist/domain/graph-contexts.js +159 -0
- package/dist/domain/graph-layout.js +43 -17
- package/dist/infrastructure/config.js +4 -0
- package/dist/mcp/server.js +11 -1
- package/dist/mcp/tools.js +64 -5
- package/docs/AGENT_USAGE.md +7 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -78,10 +78,10 @@ http://127.0.0.1:4321/
|
|
|
78
78
|
http://127.0.0.1:4321/api/graph
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
The graph server watches Markdown files by default while editing notes:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
npm run dev -- server --vault ./vault
|
|
84
|
+
npm run dev -- server --vault ./vault
|
|
85
85
|
npm run dev -- watch --vault ./vault
|
|
86
86
|
npm run dev -- bench --vault ./vault
|
|
87
87
|
npm run dev -- bench --vault ./vault --watch
|
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ blink search "jwt auth" --vault ./vault
|
|
|
265
265
|
|
|
266
266
|
blink context "how does auth work?" --vault ./vault
|
|
267
267
|
|
|
268
|
-
blink server --vault ./vault
|
|
268
|
+
blink server --vault ./vault
|
|
269
269
|
```
|
|
270
270
|
|
|
271
271
|
Open the graph UI:
|
|
@@ -536,6 +536,7 @@ Available tools:
|
|
|
536
536
|
- `brainlink_resolve_duplicate`: resolve duplicate pairs (`merge`, `link`, `ignore`) with connectivity-safe fallback edges.
|
|
537
537
|
- `brainlink_add_note`: write durable Markdown memory and reindex.
|
|
538
538
|
- `brainlink_add_file`: ingest a local file as a note and reindex.
|
|
539
|
+
- `brainlink_canonicalize_context_links`: ensure existing notes link to inferred context hubs.
|
|
539
540
|
- `brainlink_volatile_add`: write temporary agent-decided memory with TTL; volatile sections are included in context and never create durable graph edges.
|
|
540
541
|
- `brainlink_volatile_clear`: clear temporary memory for the current vault/agent namespace.
|
|
541
542
|
- `brainlink_index`: rebuild the vault index. Pass `full=true` for a complete source reindex.
|
|
@@ -543,6 +544,7 @@ Available tools:
|
|
|
543
544
|
- `brainlink_validate`: validate broken links and orphan notes.
|
|
544
545
|
- `brainlink_sync`: run index, stats, validation, broken-link and orphan checks in one call.
|
|
545
546
|
- `brainlink_graph`: read indexed graph nodes and weighted links.
|
|
547
|
+
- `brainlink_graph_contexts`: list the visual graph contexts used by the local server.
|
|
546
548
|
- `brainlink_broken_links`: list unresolved wiki links.
|
|
547
549
|
- `brainlink_orphans`: list disconnected notes.
|
|
548
550
|
|
|
@@ -577,10 +579,11 @@ blink index --vault ./vault --full
|
|
|
577
579
|
Start the local frontend:
|
|
578
580
|
|
|
579
581
|
```bash
|
|
580
|
-
blink server --host 127.0.0.1 --port 4321
|
|
582
|
+
blink server --host 127.0.0.1 --port 4321
|
|
581
583
|
```
|
|
582
584
|
|
|
583
585
|
By default, the server uses `$HOME/.brainlink/vault`. Pass `--vault ./vault` only when you want to inspect a custom vault.
|
|
586
|
+
By default, the server watches Markdown files in local filesystem vaults and reindexes after note changes. Use `--no-watch` to disable realtime reindexing.
|
|
584
587
|
By default, `blink server` tries to open the graph in a native desktop GUI window:
|
|
585
588
|
- macOS: Swift + WebKit
|
|
586
589
|
- Windows: PowerShell WinForms WebBrowser
|
|
@@ -604,7 +607,7 @@ The graph UI shows:
|
|
|
604
607
|
- context selector for segment-scoped star subgraphs derived from the visual graph context
|
|
605
608
|
- graph filter matches title, path, tags and note content
|
|
606
609
|
- graph filter keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) to preserve relationship readability
|
|
607
|
-
- realtime refresh while
|
|
610
|
+
- realtime refresh while watch mode is enabled
|
|
608
611
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
609
612
|
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
610
613
|
- wheel/button zoom updates immediately at the cursor anchor without delayed focus-transition interpolation
|
|
@@ -944,15 +947,26 @@ blink watch --vault ./vault
|
|
|
944
947
|
|
|
945
948
|
Watches Markdown files and rebuilds the index when notes change.
|
|
946
949
|
|
|
950
|
+
### `canonicalize-context-links`
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
blink canonicalize-context-links --vault ./vault --dry-run
|
|
954
|
+
blink canonicalize-context-links --vault ./vault
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Ensures existing notes have canonical `## Context Links` entries to inferred context hubs such as `User Preferences Hub`, `GitHub Repositories Hub` or `Brainlink Hub`. The command is idempotent, creates missing hub notes by default, and fully reindexes after writes unless `--no-index` is passed.
|
|
958
|
+
|
|
947
959
|
### `server`
|
|
948
960
|
|
|
949
961
|
```bash
|
|
950
|
-
blink server
|
|
951
|
-
blink server --vault ./vault
|
|
952
|
-
blink server --vault ./vault --
|
|
962
|
+
blink server
|
|
963
|
+
blink server --vault ./vault
|
|
964
|
+
blink server --vault ./vault --no-open
|
|
965
|
+
blink server --vault ./vault --no-watch
|
|
953
966
|
```
|
|
954
967
|
|
|
955
968
|
Starts the local read-only graph UI and HTTP API.
|
|
969
|
+
Watch mode is enabled by default for Markdown changes in local filesystem vaults. Use `--no-watch` to run without the watcher.
|
|
956
970
|
By default, it tries to open a native desktop GUI window for the graph URL.
|
|
957
971
|
On Linux, native GUI is disabled by default; enable it with `BRAINLINK_LINUX_NATIVE_GUI=1`.
|
|
958
972
|
If native GUI launch is unavailable, it falls back to dedicated app-window mode and then browser open.
|
|
@@ -993,6 +1007,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
993
1007
|
"allowedVaults": [".brainlink-vault"],
|
|
994
1008
|
"defaultAgent": "shared",
|
|
995
1009
|
"autoIndexOnWrite": true,
|
|
1010
|
+
"autoCanonicalContextLinks": true,
|
|
996
1011
|
"defaultSearchLimit": 10,
|
|
997
1012
|
"defaultContextTokens": 2000,
|
|
998
1013
|
"embeddingProvider": "local",
|
|
@@ -1022,6 +1037,7 @@ If no `vault` is configured and no `--vault` flag is passed, Brainlink uses `$HO
|
|
|
1022
1037
|
`agentProfiles` is optional. When present, CLI and MCP resolve `mode`, `limit` and `tokens` per agent automatically, then fallback to global defaults.
|
|
1023
1038
|
|
|
1024
1039
|
`autoIndexOnWrite` is optional and defaults to `true`. Set it to `false` to defer indexing after writes.
|
|
1040
|
+
`autoCanonicalContextLinks` is optional and defaults to `true`. When enabled, `blink add`, `brainlink_add_note` and `brainlink_add_file` add a canonical `## Context Links` entry to the inferred context hub, creating that hub when needed.
|
|
1025
1041
|
|
|
1026
1042
|
Use `"embeddingProvider": "none"` when you want FTS-only indexing.
|
|
1027
1043
|
|
|
@@ -1100,7 +1116,7 @@ Local CLI:
|
|
|
1100
1116
|
|
|
1101
1117
|
```bash
|
|
1102
1118
|
npm run dev -- --help
|
|
1103
|
-
npm run dev -- server --vault .brainlink-vault
|
|
1119
|
+
npm run dev -- server --vault .brainlink-vault
|
|
1104
1120
|
```
|
|
1105
1121
|
|
|
1106
1122
|
Package smoke test:
|
|
@@ -2,6 +2,7 @@ import { writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
|
2
2
|
import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
|
|
3
3
|
import { validateNoteInput } from '../domain/note-safety.js';
|
|
4
4
|
import { ensureVault } from '../infrastructure/file-system-vault.js';
|
|
5
|
+
import { addCanonicalContextLinkToContent, ensureCanonicalContextHub } from './canonical-context-links.js';
|
|
5
6
|
const slugify = (title) => title
|
|
6
7
|
.normalize('NFKD')
|
|
7
8
|
.replace(/[\u0300-\u036f]/g, '')
|
|
@@ -28,12 +29,20 @@ export const addNoteWithMetadata = async (vaultPath, title, content, agentId = s
|
|
|
28
29
|
const sanitizedAgentId = sanitizeAgentId(agentId);
|
|
29
30
|
const filename = `agents/${sanitizedAgentId}/${slugify(title) || 'untitled'}.md`;
|
|
30
31
|
await ensureVault(vaultPath);
|
|
31
|
-
const
|
|
32
|
+
const canonical = options.autoContextLinks === false
|
|
33
|
+
? null
|
|
34
|
+
: addCanonicalContextLinkToContent(title, content.trim());
|
|
35
|
+
const hub = canonical?.changed
|
|
36
|
+
? await ensureCanonicalContextHub(vaultPath, canonical.context, sanitizedAgentId)
|
|
37
|
+
: null;
|
|
38
|
+
const note = buildNote(title, canonical?.content ?? content.trim(), sanitizedAgentId);
|
|
32
39
|
const path = await writeMarkdownFile(vaultPath, filename, note);
|
|
33
40
|
return {
|
|
34
41
|
path,
|
|
35
|
-
autoLinked: false,
|
|
36
|
-
linkTarget: null
|
|
42
|
+
autoLinked: canonical?.changed ?? false,
|
|
43
|
+
linkTarget: canonical?.changed ? canonical.hubTitle : null,
|
|
44
|
+
context: canonical?.context ?? null,
|
|
45
|
+
hubCreated: hub?.created ?? false
|
|
37
46
|
};
|
|
38
47
|
};
|
|
39
48
|
export const addNote = async (vaultPath, title, content, agentId = sharedAgentId, options = {}) => (await addNoteWithMetadata(vaultPath, title, content, agentId, options)).path;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { inferVisualGraphContext } from '../domain/graph-contexts.js';
|
|
4
|
+
import { sanitizeAgentId, sharedAgentId } from '../domain/agents.js';
|
|
5
|
+
import { extractContextLinkWeights, parseMarkdownDocument } from '../domain/markdown.js';
|
|
6
|
+
import { ensureVault, readMarkdownFileSummaries, writeMarkdownFile } from '../infrastructure/file-system-vault.js';
|
|
7
|
+
const canonicalPriority = 'high';
|
|
8
|
+
const slugify = (title) => title
|
|
9
|
+
.normalize('NFKD')
|
|
10
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '');
|
|
14
|
+
export const hubTitleForContext = (contextTitle) => `${contextTitle} Hub`;
|
|
15
|
+
const hubPathForContext = (contextTitle, agentId) => {
|
|
16
|
+
if (contextTitle === 'GitHub Repositories')
|
|
17
|
+
return 'github-repos/github-repositories-hub.md';
|
|
18
|
+
if (contextTitle === 'GitHub Organizations')
|
|
19
|
+
return 'github-org-repos/github-organizations-hub.md';
|
|
20
|
+
if (contextTitle === 'Machine Configuration')
|
|
21
|
+
return 'machine-config/machine-configuration-hub.md';
|
|
22
|
+
return join('agents', sanitizeAgentId(agentId), `${slugify(hubTitleForContext(contextTitle))}.md`).replaceAll('\\', '/');
|
|
23
|
+
};
|
|
24
|
+
const normalizeTitle = (title) => title.trim().replace(/\.md$/i, '').toLowerCase();
|
|
25
|
+
const hasCanonicalLink = (content, hubTitle) => extractContextLinkWeights(content).some((link) => normalizeTitle(link.title) === normalizeTitle(hubTitle));
|
|
26
|
+
const linkLine = (hubTitle) => `- [[${hubTitle}]] priority: ${canonicalPriority}`;
|
|
27
|
+
const contextLinksHeading = (line) => line.match(/^(#{2,6})\s+(Context Links|Links de Contexto)\s*$/i);
|
|
28
|
+
export const upsertCanonicalContextLink = (content, hubTitle) => {
|
|
29
|
+
if (hasCanonicalLink(content, hubTitle)) {
|
|
30
|
+
return content;
|
|
31
|
+
}
|
|
32
|
+
const lines = content.replace(/\s+$/u, '').split('\n');
|
|
33
|
+
const headingIndex = lines.findIndex((line) => contextLinksHeading(line.trim()));
|
|
34
|
+
if (headingIndex === -1) {
|
|
35
|
+
return `${lines.join('\n')}\n\n## Context Links\n\n${linkLine(hubTitle)}\n`;
|
|
36
|
+
}
|
|
37
|
+
const heading = contextLinksHeading(lines[headingIndex].trim());
|
|
38
|
+
const headingDepth = heading?.[1]?.length ?? 2;
|
|
39
|
+
const insertIndex = lines.findIndex((line, index) => {
|
|
40
|
+
if (index <= headingIndex)
|
|
41
|
+
return false;
|
|
42
|
+
const candidate = line.match(/^(#{2,6})\s+/);
|
|
43
|
+
return Boolean(candidate && candidate[1].length <= headingDepth);
|
|
44
|
+
});
|
|
45
|
+
const targetIndex = insertIndex === -1 ? lines.length : insertIndex;
|
|
46
|
+
const before = lines.slice(0, targetIndex);
|
|
47
|
+
const after = lines.slice(targetIndex);
|
|
48
|
+
const needsSpacer = before[before.length - 1]?.trim() !== '';
|
|
49
|
+
const nextLines = [...before, ...(needsSpacer ? [''] : []), linkLine(hubTitle), ...after];
|
|
50
|
+
return `${nextLines.join('\n').replace(/\s+$/u, '')}\n`;
|
|
51
|
+
};
|
|
52
|
+
const buildHubContent = (hubTitle, contextTitle, agentId) => [
|
|
53
|
+
'---',
|
|
54
|
+
`title: "${hubTitle.replaceAll('"', '\\"')}"`,
|
|
55
|
+
`agent: "${sanitizeAgentId(agentId)}"`,
|
|
56
|
+
'---',
|
|
57
|
+
'',
|
|
58
|
+
`# ${hubTitle}`,
|
|
59
|
+
'',
|
|
60
|
+
`Canonical hub for the ${contextTitle} context. #memory #hub`,
|
|
61
|
+
''
|
|
62
|
+
].join('\n');
|
|
63
|
+
const readNotes = async (vaultPath) => {
|
|
64
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
65
|
+
const summaries = await readMarkdownFileSummaries(absoluteVaultPath);
|
|
66
|
+
return Promise.all(summaries.map(async (summary) => {
|
|
67
|
+
const content = await readFile(summary.absolutePath, 'utf8');
|
|
68
|
+
const document = parseMarkdownDocument({
|
|
69
|
+
absolutePath: summary.absolutePath,
|
|
70
|
+
vaultPath: absoluteVaultPath,
|
|
71
|
+
content,
|
|
72
|
+
createdAt: summary.createdAt,
|
|
73
|
+
updatedAt: summary.updatedAt
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
summary,
|
|
77
|
+
content,
|
|
78
|
+
document
|
|
79
|
+
};
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
export const ensureCanonicalContextHub = async (vaultPath, contextTitle, agentId = sharedAgentId) => {
|
|
83
|
+
const hubTitle = hubTitleForContext(contextTitle);
|
|
84
|
+
const notes = await readNotes(vaultPath);
|
|
85
|
+
const existing = notes.find((note) => normalizeTitle(note.document.title) === normalizeTitle(hubTitle));
|
|
86
|
+
const hubPath = existing?.summary.relativePath ?? hubPathForContext(contextTitle, agentId);
|
|
87
|
+
if (existing) {
|
|
88
|
+
return {
|
|
89
|
+
created: false,
|
|
90
|
+
title: hubTitle,
|
|
91
|
+
path: hubPath
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const path = await writeMarkdownFile(vaultPath, hubPath, buildHubContent(hubTitle, contextTitle, agentId));
|
|
95
|
+
return {
|
|
96
|
+
created: true,
|
|
97
|
+
title: hubTitle,
|
|
98
|
+
path
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
export const canonicalizeContextLinks = async (vaultPath, options = {}) => {
|
|
102
|
+
const agentId = options.agentId ? sanitizeAgentId(options.agentId) : undefined;
|
|
103
|
+
const createMissingHubs = options.createMissingHubs !== false;
|
|
104
|
+
const notes = await readNotes(vaultPath);
|
|
105
|
+
const scopedNotes = agentId ? notes.filter((note) => note.document.agentId === agentId) : notes;
|
|
106
|
+
const knownTitles = new Set(notes.map((note) => normalizeTitle(note.document.title)));
|
|
107
|
+
const entries = [];
|
|
108
|
+
const ensureHub = async (contextTitle, hubTitle, targetAgentId) => {
|
|
109
|
+
if (knownTitles.has(normalizeTitle(hubTitle))) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const path = hubPathForContext(contextTitle, targetAgentId);
|
|
113
|
+
if (!createMissingHubs) {
|
|
114
|
+
entries.push({
|
|
115
|
+
path,
|
|
116
|
+
title: hubTitle,
|
|
117
|
+
context: contextTitle,
|
|
118
|
+
hubTitle,
|
|
119
|
+
changed: false,
|
|
120
|
+
reason: 'missing-hub'
|
|
121
|
+
});
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
knownTitles.add(normalizeTitle(hubTitle));
|
|
125
|
+
if (!options.dryRun) {
|
|
126
|
+
await writeMarkdownFile(vaultPath, path, buildHubContent(hubTitle, contextTitle, targetAgentId));
|
|
127
|
+
}
|
|
128
|
+
entries.push({
|
|
129
|
+
path,
|
|
130
|
+
title: hubTitle,
|
|
131
|
+
context: contextTitle,
|
|
132
|
+
hubTitle,
|
|
133
|
+
changed: true,
|
|
134
|
+
reason: 'created-hub'
|
|
135
|
+
});
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
for (const note of scopedNotes) {
|
|
139
|
+
const context = inferVisualGraphContext(note.document);
|
|
140
|
+
const hubTitle = hubTitleForContext(context.title);
|
|
141
|
+
const isHub = normalizeTitle(note.document.title) === normalizeTitle(hubTitle);
|
|
142
|
+
if (isHub) {
|
|
143
|
+
entries.push({
|
|
144
|
+
path: note.summary.relativePath,
|
|
145
|
+
title: note.document.title,
|
|
146
|
+
context: context.title,
|
|
147
|
+
hubTitle,
|
|
148
|
+
changed: false,
|
|
149
|
+
reason: 'hub-note'
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const hubAvailable = await ensureHub(context.title, hubTitle, note.document.agentId || sharedAgentId);
|
|
154
|
+
if (!hubAvailable) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (hasCanonicalLink(note.content, hubTitle)) {
|
|
158
|
+
entries.push({
|
|
159
|
+
path: note.summary.relativePath,
|
|
160
|
+
title: note.document.title,
|
|
161
|
+
context: context.title,
|
|
162
|
+
hubTitle,
|
|
163
|
+
changed: false,
|
|
164
|
+
reason: 'already-linked'
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const nextContent = upsertCanonicalContextLink(note.content, hubTitle);
|
|
169
|
+
if (!options.dryRun) {
|
|
170
|
+
await writeMarkdownFile(vaultPath, note.summary.relativePath, nextContent);
|
|
171
|
+
}
|
|
172
|
+
entries.push({
|
|
173
|
+
path: note.summary.relativePath,
|
|
174
|
+
title: note.document.title,
|
|
175
|
+
context: context.title,
|
|
176
|
+
hubTitle,
|
|
177
|
+
changed: true,
|
|
178
|
+
reason: 'added-context-link'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
const changed = entries.filter((entry) => entry.changed).length;
|
|
182
|
+
const createdHubs = entries.filter((entry) => entry.reason === 'created-hub' && entry.changed).length;
|
|
183
|
+
return {
|
|
184
|
+
dryRun: options.dryRun === true,
|
|
185
|
+
scanned: scopedNotes.length,
|
|
186
|
+
changed,
|
|
187
|
+
createdHubs,
|
|
188
|
+
skipped: entries.length - changed,
|
|
189
|
+
entries
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
export const addCanonicalContextLinkToContent = (title, content) => {
|
|
193
|
+
const context = inferVisualGraphContext({
|
|
194
|
+
id: '',
|
|
195
|
+
agentId: sharedAgentId,
|
|
196
|
+
title,
|
|
197
|
+
path: '',
|
|
198
|
+
content,
|
|
199
|
+
tags: [],
|
|
200
|
+
});
|
|
201
|
+
const hubTitle = hubTitleForContext(context.title);
|
|
202
|
+
const nextContent = normalizeTitle(title) === normalizeTitle(hubTitle) ? content : upsertCanonicalContextLink(content, hubTitle);
|
|
203
|
+
return {
|
|
204
|
+
content: nextContent,
|
|
205
|
+
context: context.title,
|
|
206
|
+
hubTitle,
|
|
207
|
+
changed: nextContent !== content
|
|
208
|
+
};
|
|
209
|
+
};
|
|
@@ -99,10 +99,87 @@ select {
|
|
|
99
99
|
cursor: grab;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
#graph.is-node-hover {
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#graph.is-node-dragging {
|
|
107
|
+
cursor: move;
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
#graph:active {
|
|
103
111
|
cursor: grabbing;
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
.graph-labels {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: 0;
|
|
117
|
+
pointer-events: none;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.graph-label {
|
|
122
|
+
position: absolute;
|
|
123
|
+
max-width: 220px;
|
|
124
|
+
transform: translate(-50%, calc(-100% - 10px));
|
|
125
|
+
padding: 4px 7px;
|
|
126
|
+
border: 1px solid rgba(129, 146, 170, 0.28);
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
background: rgba(13, 16, 20, 0.78);
|
|
129
|
+
color: var(--text);
|
|
130
|
+
font-size: 11px;
|
|
131
|
+
line-height: 1.25;
|
|
132
|
+
white-space: nowrap;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.28);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.graph-label.is-focused {
|
|
139
|
+
border-color: rgba(53, 208, 162, 0.72);
|
|
140
|
+
color: #dffbf3;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.graph-tooltip {
|
|
144
|
+
position: absolute;
|
|
145
|
+
z-index: 4;
|
|
146
|
+
max-width: min(320px, calc(100vw - 32px));
|
|
147
|
+
padding: 8px 10px;
|
|
148
|
+
border: 1px solid var(--line);
|
|
149
|
+
border-radius: 6px;
|
|
150
|
+
background: rgba(13, 16, 20, 0.94);
|
|
151
|
+
color: var(--text);
|
|
152
|
+
font-size: 12px;
|
|
153
|
+
line-height: 1.35;
|
|
154
|
+
pointer-events: none;
|
|
155
|
+
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.38);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.graph-tooltip strong,
|
|
159
|
+
.graph-tooltip small {
|
|
160
|
+
display: block;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
text-overflow: ellipsis;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.graph-tooltip small {
|
|
166
|
+
margin-top: 3px;
|
|
167
|
+
color: var(--muted);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.mini-map {
|
|
171
|
+
position: absolute;
|
|
172
|
+
right: 14px;
|
|
173
|
+
bottom: 14px;
|
|
174
|
+
z-index: 3;
|
|
175
|
+
width: 180px;
|
|
176
|
+
height: 120px;
|
|
177
|
+
border: 1px solid rgba(129, 146, 170, 0.28);
|
|
178
|
+
border-radius: 8px;
|
|
179
|
+
background: rgba(13, 16, 20, 0.78);
|
|
180
|
+
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.38);
|
|
181
|
+
}
|
|
182
|
+
|
|
106
183
|
.eyebrow {
|
|
107
184
|
color: var(--muted);
|
|
108
185
|
font-size: 12px;
|
|
@@ -42,12 +42,16 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
42
42
|
<button id="zoomIn" type="button" title="Zoom in">+</button>
|
|
43
43
|
<button id="zoomOut" type="button" title="Zoom out">-</button>
|
|
44
44
|
<button id="fit" type="button" title="Focus central hub">◎</button>
|
|
45
|
+
<button id="releaseNode" type="button" title="Release selected node">◇</button>
|
|
45
46
|
<button id="reset" type="button" title="Reset view">⌂</button>
|
|
46
47
|
</div>
|
|
47
48
|
</div>
|
|
48
49
|
</header>
|
|
49
50
|
<div class="graph-stage">
|
|
50
51
|
<canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
|
|
52
|
+
<div id="graphLabels" class="graph-labels" aria-hidden="true"></div>
|
|
53
|
+
<div id="graphTooltip" class="graph-tooltip" role="tooltip" hidden></div>
|
|
54
|
+
<canvas id="miniMap" class="mini-map" aria-label="Graph overview"></canvas>
|
|
51
55
|
</div>
|
|
52
56
|
</section>
|
|
53
57
|
</main>
|