@andespindola/brainlink 0.1.0-beta.0 → 0.1.0-beta.10
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/CHANGELOG.md +46 -0
- package/README.md +252 -19
- package/dist/application/add-note.js +62 -13
- package/dist/application/analyze-vault.js +104 -9
- package/dist/application/frontend/client-css.js +154 -71
- package/dist/application/frontend/client-html.js +42 -33
- package/dist/application/frontend/client-js.js +255 -70
- package/dist/application/get-graph-layout.js +6 -3
- package/dist/application/get-graph-node.js +12 -0
- package/dist/application/get-graph-summary.js +12 -0
- package/dist/application/migrate-vault.js +91 -0
- package/dist/application/search-graph-node-ids.js +12 -0
- package/dist/application/search-knowledge.js +56 -1
- package/dist/application/server/routes.js +27 -1
- package/dist/cli/commands/agent-commands.js +412 -0
- package/dist/cli/commands/config-commands.js +167 -0
- package/dist/cli/commands/read-commands.js +25 -8
- package/dist/cli/commands/write-commands.js +191 -7
- package/dist/cli/main.js +4 -0
- package/dist/cli/runtime.js +5 -2
- package/dist/domain/embeddings.js +2 -1
- package/dist/domain/graph-layout.js +20 -14
- package/dist/domain/markdown.js +36 -4
- package/dist/infrastructure/config.js +96 -8
- package/dist/infrastructure/file-system-vault.js +15 -0
- package/dist/infrastructure/paths.js +9 -1
- package/dist/infrastructure/session-state.js +172 -0
- package/dist/infrastructure/sqlite/graph-reader.js +252 -105
- package/dist/infrastructure/sqlite/recovery.js +83 -0
- package/dist/infrastructure/sqlite/schema.js +4 -1
- package/dist/infrastructure/sqlite/search-reader.js +104 -72
- package/dist/infrastructure/sqlite-index.js +16 -3
- package/dist/mcp/main.js +11 -3
- package/dist/mcp/server.js +22 -2
- package/dist/mcp/startup.js +35 -0
- package/dist/mcp/tools.js +617 -21
- package/docs/AGENT_USAGE.md +95 -6
- package/docs/ARCHITECTURE.md +15 -1
- package/docs/QUICKSTART.md +104 -0
- package/docs/RELEASE.md +3 -3
- package/package.json +1 -1
|
@@ -1,10 +1,91 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { join } from 'node:path';
|
|
1
5
|
import { validateGraph, getBrokenLinks, getOrphanNodes, getVaultStats } from '../domain/graph-analysis.js';
|
|
2
|
-
import { ensureVault, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { ensureVault, listVaultFiles, readMarkdownFiles } from '../infrastructure/file-system-vault.js';
|
|
7
|
+
import { resolveAgentRuntimeDefaults } from '../infrastructure/config.js';
|
|
8
|
+
import { getGraphSummary } from './get-graph-summary.js';
|
|
9
|
+
import { buildContextPackage } from './build-context.js';
|
|
10
|
+
import { indexVault } from './index-vault.js';
|
|
11
|
+
import { searchKnowledge } from './search-knowledge.js';
|
|
12
|
+
import { loadBrainlinkConfig } from '../infrastructure/config.js';
|
|
13
|
+
export const getStats = async (vaultPath, agentId) => getVaultStats(await getGraphSummary(vaultPath, agentId));
|
|
14
|
+
export const getBrokenLinksReport = async (vaultPath, agentId) => getBrokenLinks(await getGraphSummary(vaultPath, agentId));
|
|
15
|
+
export const getOrphansReport = async (vaultPath, agentId) => getOrphanNodes(await getGraphSummary(vaultPath, agentId));
|
|
16
|
+
export const validateVault = async (vaultPath, agentId) => validateGraph(await getGraphSummary(vaultPath, agentId));
|
|
17
|
+
const toRatio = (part, total) => total === 0 ? 0 : Number((part / total).toFixed(4));
|
|
18
|
+
export const getExtendedStats = async (vaultPath, agentId) => {
|
|
19
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
20
|
+
const graph = await getGraphSummary(absoluteVaultPath, agentId);
|
|
21
|
+
const stats = getVaultStats(graph);
|
|
22
|
+
const markdownFiles = await readMarkdownFiles(absoluteVaultPath);
|
|
23
|
+
const allFiles = await listVaultFiles(absoluteVaultPath);
|
|
24
|
+
const totalBytes = (await Promise.all(allFiles.map(async (filePath) => {
|
|
25
|
+
try {
|
|
26
|
+
return (await stat(filePath)).size;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
}))).reduce((sum, value) => sum + value, 0);
|
|
32
|
+
const updatedAt = markdownFiles
|
|
33
|
+
.map((file) => file.updatedAt.getTime())
|
|
34
|
+
.filter((time) => Number.isFinite(time))
|
|
35
|
+
.sort((left, right) => left - right);
|
|
36
|
+
const priorities = graph.edges.reduce((state, edge) => ({
|
|
37
|
+
...state,
|
|
38
|
+
[edge.priority]: state[edge.priority] + 1
|
|
39
|
+
}), {
|
|
40
|
+
low: 0,
|
|
41
|
+
normal: 0,
|
|
42
|
+
high: 0,
|
|
43
|
+
critical: 0
|
|
44
|
+
});
|
|
45
|
+
const config = await loadBrainlinkConfig();
|
|
46
|
+
const defaults = resolveAgentRuntimeDefaults(config, agentId);
|
|
47
|
+
const probeQuery = graph.nodes[0]?.title ?? 'architecture';
|
|
48
|
+
const indexStart = performance.now();
|
|
49
|
+
await indexVault(absoluteVaultPath);
|
|
50
|
+
const indexLatency = performance.now() - indexStart;
|
|
51
|
+
const searchStart = performance.now();
|
|
52
|
+
await searchKnowledge(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), agentId, 'hybrid');
|
|
53
|
+
const searchLatency = performance.now() - searchStart;
|
|
54
|
+
const contextStart = performance.now();
|
|
55
|
+
await buildContextPackage(absoluteVaultPath, probeQuery, Math.min(defaults.defaultSearchLimit, 8), defaults.defaultContextTokens, agentId, 'hybrid');
|
|
56
|
+
const contextLatency = performance.now() - contextStart;
|
|
57
|
+
return {
|
|
58
|
+
stats,
|
|
59
|
+
storage: {
|
|
60
|
+
markdownFileCount: markdownFiles.length,
|
|
61
|
+
totalFileCount: allFiles.length,
|
|
62
|
+
totalBytes,
|
|
63
|
+
averageMarkdownBytes: markdownFiles.length === 0
|
|
64
|
+
? 0
|
|
65
|
+
: Math.round(markdownFiles.reduce((sum, file) => sum + Buffer.byteLength(file.content, 'utf8'), 0) / markdownFiles.length),
|
|
66
|
+
...(updatedAt.length > 0
|
|
67
|
+
? {
|
|
68
|
+
oldestNoteUpdatedAt: new Date(updatedAt[0]).toISOString(),
|
|
69
|
+
newestNoteUpdatedAt: new Date(updatedAt[updatedAt.length - 1]).toISOString()
|
|
70
|
+
}
|
|
71
|
+
: {})
|
|
72
|
+
},
|
|
73
|
+
quality: {
|
|
74
|
+
resolvedLinkRatio: toRatio(stats.resolvedLinkCount, stats.linkCount),
|
|
75
|
+
brokenLinkRatio: toRatio(stats.brokenLinkCount, stats.linkCount),
|
|
76
|
+
orphanRatio: toRatio(stats.orphanCount, Math.max(stats.documentCount, 1)),
|
|
77
|
+
priorityDistribution: priorities
|
|
78
|
+
},
|
|
79
|
+
observability: {
|
|
80
|
+
probeQuery,
|
|
81
|
+
latenciesMs: {
|
|
82
|
+
index: Number(indexLatency.toFixed(2)),
|
|
83
|
+
search: Number(searchLatency.toFixed(2)),
|
|
84
|
+
context: Number(contextLatency.toFixed(2))
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
8
89
|
const createCheck = (name, ok, message) => ({
|
|
9
90
|
name,
|
|
10
91
|
ok,
|
|
@@ -13,16 +94,30 @@ const createCheck = (name, ok, message) => ({
|
|
|
13
94
|
export const doctorVault = async (vaultPath) => {
|
|
14
95
|
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
15
96
|
const files = await readMarkdownFiles(absoluteVaultPath);
|
|
16
|
-
const graph = await
|
|
97
|
+
const graph = await getGraphSummary(absoluteVaultPath);
|
|
17
98
|
const validation = validateGraph(graph);
|
|
99
|
+
const backupPath = join(absoluteVaultPath, '.brainlink', 'brainlink.db.backup');
|
|
100
|
+
const hasBackup = existsSync(backupPath);
|
|
101
|
+
const backupReady = graph.nodes.length === 0 || hasBackup;
|
|
18
102
|
const checks = [
|
|
19
103
|
createCheck('vault', true, `Vault ready at ${absoluteVaultPath}`),
|
|
20
104
|
createCheck('markdown-files', files.length > 0, `${files.length} markdown files found`),
|
|
21
105
|
createCheck('index', graph.nodes.length > 0, `${graph.nodes.length} indexed documents found`),
|
|
22
|
-
createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`)
|
|
106
|
+
createCheck('broken-links', validation.brokenLinks.length === 0, `${validation.brokenLinks.length} broken links found`),
|
|
107
|
+
createCheck('index-backup', backupReady, backupReady
|
|
108
|
+
? (hasBackup ? 'SQLite recovery snapshot is available' : 'No index yet. Snapshot will be created after first indexing run')
|
|
109
|
+
: 'Recovery snapshot missing. Run blink index to create a rollback snapshot')
|
|
23
110
|
];
|
|
111
|
+
const recommendations = files.length === 0 && graph.nodes.length === 0
|
|
112
|
+
? [
|
|
113
|
+
`Vault is empty. Add your first note: blink add "Architecture" --vault "${absoluteVaultPath}" --content "Markdown source of truth. #architecture"`,
|
|
114
|
+
`If this path is not the expected vault, inspect active config: blink config where`,
|
|
115
|
+
`If you changed vault recently, migrate existing memory: blink migrate-vault --from ~/.brainlink/vault --to "${absoluteVaultPath}"`
|
|
116
|
+
]
|
|
117
|
+
: [];
|
|
24
118
|
return {
|
|
25
119
|
ok: checks.every((check) => check.ok),
|
|
26
|
-
checks
|
|
120
|
+
checks,
|
|
121
|
+
...(recommendations.length > 0 ? { recommendations } : {})
|
|
27
122
|
};
|
|
28
123
|
};
|
|
@@ -32,8 +32,6 @@ select {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.shell {
|
|
35
|
-
display: grid;
|
|
36
|
-
grid-template-columns: minmax(0, 1fr) 360px;
|
|
37
35
|
width: 100%;
|
|
38
36
|
height: 100svh;
|
|
39
37
|
overflow: hidden;
|
|
@@ -73,17 +71,14 @@ select {
|
|
|
73
71
|
|
|
74
72
|
.topbar > div {
|
|
75
73
|
display: flex;
|
|
76
|
-
align-items:
|
|
77
|
-
gap: 12px;
|
|
74
|
+
align-items: center;
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
.topbar strong {
|
|
81
78
|
font-size: 18px;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
|
-
.
|
|
85
|
-
.eyebrow,
|
|
86
|
-
.inspector small {
|
|
81
|
+
.eyebrow {
|
|
87
82
|
color: var(--muted);
|
|
88
83
|
font-size: 12px;
|
|
89
84
|
}
|
|
@@ -138,70 +133,37 @@ select {
|
|
|
138
133
|
color: var(--accent);
|
|
139
134
|
}
|
|
140
135
|
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
border-left: 1px solid var(--line);
|
|
149
|
-
background: var(--panel);
|
|
150
|
-
overflow: auto;
|
|
136
|
+
.floating-metrics {
|
|
137
|
+
position: absolute;
|
|
138
|
+
top: 66px;
|
|
139
|
+
left: 18px;
|
|
140
|
+
display: flex;
|
|
141
|
+
gap: 10px;
|
|
142
|
+
pointer-events: none;
|
|
151
143
|
}
|
|
152
144
|
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
.metric-chip {
|
|
146
|
+
min-width: 94px;
|
|
147
|
+
padding: 10px 12px;
|
|
148
|
+
border: 1px solid var(--line);
|
|
149
|
+
border-radius: 10px;
|
|
150
|
+
background: rgba(21, 25, 31, 0.88);
|
|
151
|
+
display: grid;
|
|
152
|
+
gap: 3px;
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
.
|
|
160
|
-
margin-top: 6px;
|
|
155
|
+
.metric-chip strong {
|
|
161
156
|
font-size: 26px;
|
|
162
|
-
line-height: 1
|
|
163
|
-
overflow-wrap: anywhere;
|
|
157
|
+
line-height: 1;
|
|
164
158
|
}
|
|
165
159
|
|
|
166
|
-
.
|
|
167
|
-
margin-bottom: 10px;
|
|
160
|
+
.metric-chip small {
|
|
168
161
|
color: var(--muted);
|
|
169
|
-
font-size:
|
|
170
|
-
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
letter-spacing: 0.03em;
|
|
171
164
|
text-transform: uppercase;
|
|
172
165
|
}
|
|
173
166
|
|
|
174
|
-
#path {
|
|
175
|
-
margin-top: 10px;
|
|
176
|
-
color: var(--muted);
|
|
177
|
-
line-height: 1.45;
|
|
178
|
-
overflow-wrap: anywhere;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
.metrics {
|
|
182
|
-
display: grid;
|
|
183
|
-
grid-template-columns: repeat(3, 1fr);
|
|
184
|
-
border: 1px solid var(--line);
|
|
185
|
-
border-radius: 8px;
|
|
186
|
-
overflow: hidden;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.metrics div {
|
|
190
|
-
display: grid;
|
|
191
|
-
gap: 4px;
|
|
192
|
-
padding: 14px;
|
|
193
|
-
background: var(--panel-strong);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.metrics div + div {
|
|
197
|
-
border-left: 1px solid var(--line);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
.metrics span {
|
|
201
|
-
font-size: 22px;
|
|
202
|
-
font-weight: 700;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
167
|
.tags {
|
|
206
168
|
display: flex;
|
|
207
169
|
flex-wrap: wrap;
|
|
@@ -215,6 +177,7 @@ select {
|
|
|
215
177
|
background: var(--accent-weak);
|
|
216
178
|
color: var(--accent);
|
|
217
179
|
font-size: 12px;
|
|
180
|
+
word-break: break-word;
|
|
218
181
|
overflow-wrap: anywhere;
|
|
219
182
|
}
|
|
220
183
|
|
|
@@ -230,6 +193,7 @@ li {
|
|
|
230
193
|
padding: 10px 0;
|
|
231
194
|
border-bottom: 1px solid var(--line);
|
|
232
195
|
color: var(--text);
|
|
196
|
+
word-break: break-word;
|
|
233
197
|
overflow-wrap: anywhere;
|
|
234
198
|
}
|
|
235
199
|
|
|
@@ -253,7 +217,7 @@ li small {
|
|
|
253
217
|
}
|
|
254
218
|
|
|
255
219
|
.note-content {
|
|
256
|
-
max-height:
|
|
220
|
+
max-height: min(68svh, 760px);
|
|
257
221
|
margin: 0;
|
|
258
222
|
padding: 12px;
|
|
259
223
|
border: 1px solid var(--line);
|
|
@@ -267,18 +231,116 @@ li small {
|
|
|
267
231
|
line-height: 1.5;
|
|
268
232
|
}
|
|
269
233
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
234
|
+
.content-dialog {
|
|
235
|
+
width: min(920px, calc(100vw - 32px));
|
|
236
|
+
max-height: calc(100svh - 32px);
|
|
237
|
+
padding: 0;
|
|
238
|
+
border: 1px solid var(--line);
|
|
239
|
+
border-radius: 8px;
|
|
240
|
+
background: var(--panel);
|
|
241
|
+
color: var(--text);
|
|
242
|
+
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.48);
|
|
243
|
+
}
|
|
275
244
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
245
|
+
.content-dialog::backdrop {
|
|
246
|
+
background: rgba(4, 7, 10, 0.72);
|
|
247
|
+
backdrop-filter: blur(4px);
|
|
248
|
+
}
|
|
281
249
|
|
|
250
|
+
.content-dialog article {
|
|
251
|
+
display: grid;
|
|
252
|
+
grid-template-rows: auto auto minmax(0, 1fr);
|
|
253
|
+
max-height: calc(100svh - 34px);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.content-dialog header {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: flex-start;
|
|
259
|
+
justify-content: space-between;
|
|
260
|
+
gap: 18px;
|
|
261
|
+
padding: 22px;
|
|
262
|
+
border-bottom: 1px solid var(--line);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.content-dialog h2,
|
|
266
|
+
.content-dialog p {
|
|
267
|
+
margin: 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.content-dialog h2 {
|
|
271
|
+
margin-top: 6px;
|
|
272
|
+
font-size: 24px;
|
|
273
|
+
line-height: 1.15;
|
|
274
|
+
overflow-wrap: anywhere;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.content-dialog p {
|
|
278
|
+
margin-top: 8px;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
overflow-wrap: anywhere;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.content-dialog button {
|
|
284
|
+
flex: 0 0 auto;
|
|
285
|
+
height: 38px;
|
|
286
|
+
padding: 0 14px;
|
|
287
|
+
border: 1px solid var(--line);
|
|
288
|
+
border-radius: 8px;
|
|
289
|
+
background: var(--panel-strong);
|
|
290
|
+
color: var(--text);
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.content-dialog button:hover,
|
|
295
|
+
.content-dialog button:focus {
|
|
296
|
+
border-color: var(--accent);
|
|
297
|
+
color: var(--accent);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.content-meta {
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
303
|
+
gap: 10px;
|
|
304
|
+
padding: 14px 22px;
|
|
305
|
+
border-bottom: 1px solid var(--line);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.content-meta-section {
|
|
309
|
+
min-height: 0;
|
|
310
|
+
padding: 10px;
|
|
311
|
+
border: 1px solid var(--line);
|
|
312
|
+
border-radius: 8px;
|
|
313
|
+
background: var(--panel-strong);
|
|
314
|
+
display: grid;
|
|
315
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
316
|
+
gap: 8px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.content-meta-section h3 {
|
|
320
|
+
margin: 0;
|
|
321
|
+
color: var(--muted);
|
|
322
|
+
font-size: 11px;
|
|
323
|
+
font-weight: 700;
|
|
324
|
+
text-transform: uppercase;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.content-meta-section ul,
|
|
328
|
+
.content-meta-section .tags {
|
|
329
|
+
max-height: 140px;
|
|
330
|
+
overflow: auto;
|
|
331
|
+
align-content: flex-start;
|
|
332
|
+
padding-right: 4px;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.content-dialog .note-content {
|
|
336
|
+
max-height: none;
|
|
337
|
+
min-height: 0;
|
|
338
|
+
border: 0;
|
|
339
|
+
border-radius: 0;
|
|
340
|
+
padding: 22px;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@media (max-width: 860px) {
|
|
282
344
|
.topbar {
|
|
283
345
|
align-items: stretch;
|
|
284
346
|
flex-direction: column;
|
|
@@ -291,4 +353,25 @@ li small {
|
|
|
291
353
|
.agent-filter {
|
|
292
354
|
width: 100%;
|
|
293
355
|
}
|
|
356
|
+
|
|
357
|
+
.content-dialog header {
|
|
358
|
+
align-items: stretch;
|
|
359
|
+
flex-direction: column;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.floating-metrics {
|
|
363
|
+
top: 116px;
|
|
364
|
+
right: 18px;
|
|
365
|
+
left: 18px;
|
|
366
|
+
justify-content: flex-start;
|
|
367
|
+
flex-wrap: wrap;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.metric-chip {
|
|
371
|
+
min-width: 82px;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.content-meta {
|
|
375
|
+
grid-template-columns: 1fr;
|
|
376
|
+
}
|
|
294
377
|
}`;
|
|
@@ -13,7 +13,6 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
13
13
|
<div class="topbar">
|
|
14
14
|
<div>
|
|
15
15
|
<strong>Brainlink</strong>
|
|
16
|
-
<span id="stats">Loading graph</span>
|
|
17
16
|
</div>
|
|
18
17
|
<label class="search">
|
|
19
18
|
<input id="search" type="search" placeholder="Filter notes, tags or paths" autocomplete="off" />
|
|
@@ -22,45 +21,55 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
22
21
|
<select id="agent"></select>
|
|
23
22
|
</label>
|
|
24
23
|
</div>
|
|
24
|
+
<div class="floating-metrics" aria-label="Graph totals">
|
|
25
|
+
<div class="metric-chip">
|
|
26
|
+
<strong id="nodeCount">0</strong>
|
|
27
|
+
<small>Notes</small>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="metric-chip">
|
|
30
|
+
<strong id="edgeCount">0</strong>
|
|
31
|
+
<small>Links</small>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="metric-chip">
|
|
34
|
+
<strong id="tagCount">0</strong>
|
|
35
|
+
<small>Tags</small>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
25
38
|
<div class="toolbar" aria-label="Graph controls">
|
|
26
39
|
<button id="zoomIn" type="button" title="Zoom in">+</button>
|
|
27
40
|
<button id="zoomOut" type="button" title="Zoom out">-</button>
|
|
41
|
+
<button id="fit" type="button" title="Fit visible nodes">◎</button>
|
|
28
42
|
<button id="reset" type="button" title="Reset view">⌂</button>
|
|
29
43
|
</div>
|
|
30
44
|
</section>
|
|
31
|
-
<aside class="inspector" aria-label="Selected note">
|
|
32
|
-
<div>
|
|
33
|
-
<span class="eyebrow">Selected note</span>
|
|
34
|
-
<h1 id="title">Graph Overview</h1>
|
|
35
|
-
<p id="path">Select a node to inspect links and backlinks.</p>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="metrics">
|
|
38
|
-
<div><span id="nodeCount">0</span><small>Notes</small></div>
|
|
39
|
-
<div><span id="edgeCount">0</span><small>Links</small></div>
|
|
40
|
-
<div><span id="tagCount">0</span><small>Tags</small></div>
|
|
41
|
-
</div>
|
|
42
|
-
<section>
|
|
43
|
-
<h2>Tags</h2>
|
|
44
|
-
<div id="tags" class="tags"></div>
|
|
45
|
-
</section>
|
|
46
|
-
<section>
|
|
47
|
-
<h2>Notes</h2>
|
|
48
|
-
<ul id="notes"></ul>
|
|
49
|
-
</section>
|
|
50
|
-
<section>
|
|
51
|
-
<h2>Content</h2>
|
|
52
|
-
<pre id="content" class="note-content"></pre>
|
|
53
|
-
</section>
|
|
54
|
-
<section>
|
|
55
|
-
<h2>Outgoing</h2>
|
|
56
|
-
<ul id="outgoing"></ul>
|
|
57
|
-
</section>
|
|
58
|
-
<section>
|
|
59
|
-
<h2>Backlinks</h2>
|
|
60
|
-
<ul id="incoming"></ul>
|
|
61
|
-
</section>
|
|
62
|
-
</aside>
|
|
63
45
|
</main>
|
|
46
|
+
<dialog id="contentDialog" class="content-dialog" aria-labelledby="contentTitle">
|
|
47
|
+
<article>
|
|
48
|
+
<header>
|
|
49
|
+
<div>
|
|
50
|
+
<span class="eyebrow">Markdown content</span>
|
|
51
|
+
<h2 id="contentTitle">Selected note</h2>
|
|
52
|
+
<p id="contentPath"></p>
|
|
53
|
+
</div>
|
|
54
|
+
<button id="contentClose" type="button">Close</button>
|
|
55
|
+
</header>
|
|
56
|
+
<div class="content-meta">
|
|
57
|
+
<section class="content-meta-section">
|
|
58
|
+
<h3>Tags</h3>
|
|
59
|
+
<div id="contentTags" class="tags"></div>
|
|
60
|
+
</section>
|
|
61
|
+
<section class="content-meta-section">
|
|
62
|
+
<h3>Outgoing</h3>
|
|
63
|
+
<ul id="contentOutgoing"></ul>
|
|
64
|
+
</section>
|
|
65
|
+
<section class="content-meta-section">
|
|
66
|
+
<h3>Backlinks</h3>
|
|
67
|
+
<ul id="contentIncoming"></ul>
|
|
68
|
+
</section>
|
|
69
|
+
</div>
|
|
70
|
+
<pre id="contentBody" class="note-content"></pre>
|
|
71
|
+
</article>
|
|
72
|
+
</dialog>
|
|
64
73
|
<script src="/app.js"></script>
|
|
65
74
|
</body>
|
|
66
75
|
</html>`;
|