@andespindola/brainlink 1.0.5 → 1.0.7
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 +17 -0
- package/README.md +46 -0
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/find-similar-notes.js +75 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +190 -0
- package/dist/application/index-vault.js +46 -167
- package/dist/application/memory-suggestions.js +4 -1
- package/dist/application/search-knowledge.js +19 -3
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +60 -11
- package/dist/domain/diversity.js +64 -0
- package/dist/domain/embeddings.js +148 -6
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/domain/markdown.js +10 -1
- package/dist/domain/scoring.js +42 -0
- package/dist/domain/tokens.js +17 -1
- package/dist/infrastructure/config.js +33 -1
- package/dist/infrastructure/file-index.js +227 -60
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +23 -1
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +537 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +353 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/AGENT_USAGE.md +4 -1
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/QUICKSTART.md +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export const createInputJs = () => `
|
|
2
|
+
const resolvePointer = (event) => {
|
|
3
|
+
const rect = canvas.getBoundingClientRect()
|
|
4
|
+
return {
|
|
5
|
+
x: event.clientX - rect.left,
|
|
6
|
+
y: event.clientY - rect.top
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const setupInput = () => {
|
|
11
|
+
const dragActivationDistance = 6
|
|
12
|
+
const resetPointerState = (pointerId = null) => {
|
|
13
|
+
state.pointer.down = false
|
|
14
|
+
state.pointer.dragging = false
|
|
15
|
+
state.pointer.dragNodeId = ''
|
|
16
|
+
canvas.classList.remove('is-node-dragging')
|
|
17
|
+
if (pointerId !== null) {
|
|
18
|
+
try {
|
|
19
|
+
if (canvas.hasPointerCapture(pointerId)) {
|
|
20
|
+
canvas.releasePointerCapture(pointerId)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
canvas.addEventListener('wheel', (event) => {
|
|
27
|
+
event.preventDefault()
|
|
28
|
+
state.lastWheelAt = performance.now()
|
|
29
|
+
const pointer = resolvePointer(event)
|
|
30
|
+
const exponent = Math.max(-0.05, Math.min(0.05, -event.deltaY * 0.001))
|
|
31
|
+
zoomAtPoint(pointer.x, pointer.y, Math.exp(exponent))
|
|
32
|
+
}, { passive: false })
|
|
33
|
+
|
|
34
|
+
canvas.addEventListener('pointerdown', (event) => {
|
|
35
|
+
event.preventDefault()
|
|
36
|
+
const pointer = resolvePointer(event)
|
|
37
|
+
const candidateNode = pickFallbackNode(pointer.x, pointer.y)
|
|
38
|
+
const candidateNodeId = isRealGraphNode(candidateNode) && typeof candidateNode?.[0] === 'string' ? candidateNode[0] : ''
|
|
39
|
+
const candidateX = Number(candidateNode?.[2])
|
|
40
|
+
const candidateY = Number(candidateNode?.[3])
|
|
41
|
+
const world = screenToWorld(pointer.x, pointer.y)
|
|
42
|
+
state.pointer.down = true
|
|
43
|
+
state.pointer.moved = false
|
|
44
|
+
state.pointer.dragging = false
|
|
45
|
+
state.pointer.dragNodeId = candidateNodeId
|
|
46
|
+
state.pointer.x = pointer.x
|
|
47
|
+
state.pointer.y = pointer.y
|
|
48
|
+
state.pointer.startX = pointer.x
|
|
49
|
+
state.pointer.startY = pointer.y
|
|
50
|
+
state.pointer.startWorldX = world.x
|
|
51
|
+
state.pointer.startWorldY = world.y
|
|
52
|
+
state.pointer.nodeStartX = candidateNodeId && Number.isFinite(candidateX) ? candidateX : 0
|
|
53
|
+
state.pointer.nodeStartY = candidateNodeId && Number.isFinite(candidateY) ? candidateY : 0
|
|
54
|
+
state.pointer.worldAnchorX = world.x
|
|
55
|
+
state.pointer.worldAnchorY = world.y
|
|
56
|
+
try {
|
|
57
|
+
canvas.setPointerCapture(event.pointerId)
|
|
58
|
+
} catch {}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
canvas.addEventListener('pointermove', (event) => {
|
|
62
|
+
if (state.pointer.down) {
|
|
63
|
+
event.preventDefault()
|
|
64
|
+
}
|
|
65
|
+
const pointer = resolvePointer(event)
|
|
66
|
+
|
|
67
|
+
if (state.pointer.down) {
|
|
68
|
+
const dx = pointer.x - state.pointer.x
|
|
69
|
+
const dy = pointer.y - state.pointer.y
|
|
70
|
+
const distanceFromStart = Math.hypot(pointer.x - state.pointer.startX, pointer.y - state.pointer.startY)
|
|
71
|
+
if (distanceFromStart >= dragActivationDistance) {
|
|
72
|
+
state.pointer.moved = true
|
|
73
|
+
state.pointer.dragging = true
|
|
74
|
+
canvas.classList.toggle('is-node-dragging', Boolean(state.pointer.dragNodeId))
|
|
75
|
+
}
|
|
76
|
+
if (!state.pointer.dragging) {
|
|
77
|
+
state.pointer.x = pointer.x
|
|
78
|
+
state.pointer.y = pointer.y
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (state.pointer.dragNodeId) {
|
|
82
|
+
const world = screenToWorld(pointer.x, pointer.y)
|
|
83
|
+
const x = state.pointer.nodeStartX + world.x - state.pointer.startWorldX
|
|
84
|
+
const y = state.pointer.nodeStartY + world.y - state.pointer.startWorldY
|
|
85
|
+
state.nodePositions.set(state.pointer.dragNodeId, { x, y })
|
|
86
|
+
updateNodePositionInChunk(state.pointer.dragNodeId, x, y)
|
|
87
|
+
state.pointer.x = pointer.x
|
|
88
|
+
state.pointer.y = pointer.y
|
|
89
|
+
drawFallback()
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
state.camera.x += dx
|
|
93
|
+
state.camera.y += dy
|
|
94
|
+
state.pointer.x = pointer.x
|
|
95
|
+
state.pointer.y = pointer.y
|
|
96
|
+
updateWorkerCamera()
|
|
97
|
+
drawFallback()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const hovered = pickFallbackNode(pointer.x, pointer.y)
|
|
102
|
+
const hoveredId = isRealGraphNode(hovered) && typeof hovered?.[0] === 'string' ? hovered[0] : ''
|
|
103
|
+
if (state.hoveredNodeId !== hoveredId) {
|
|
104
|
+
state.hoveredNodeId = hoveredId
|
|
105
|
+
canvas.classList.toggle('is-node-hover', Boolean(hoveredId))
|
|
106
|
+
updateGraphOverlays()
|
|
107
|
+
}
|
|
108
|
+
if (hoveredId) {
|
|
109
|
+
showTooltip(hovered, pointer)
|
|
110
|
+
} else {
|
|
111
|
+
hideTooltip()
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
canvas.addEventListener('pointerup', (event) => {
|
|
116
|
+
const pointer = resolvePointer(event)
|
|
117
|
+
const distanceFromStart = Math.hypot(pointer.x - state.pointer.startX, pointer.y - state.pointer.startY)
|
|
118
|
+
const shouldPick = !state.pointer.dragging && distanceFromStart < dragActivationDistance
|
|
119
|
+
const shouldRefreshAfterDrag = state.pointer.dragging
|
|
120
|
+
const shouldPersistNodePosition = state.pointer.dragging && Boolean(state.pointer.dragNodeId)
|
|
121
|
+
resetPointerState(event.pointerId)
|
|
122
|
+
|
|
123
|
+
if (shouldPick) {
|
|
124
|
+
pickAt(pointer.x, pointer.y)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (shouldPersistNodePosition) {
|
|
128
|
+
writeStoredNodePositions()
|
|
129
|
+
persistNodePositionsToServer()
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (shouldRefreshAfterDrag) {
|
|
133
|
+
scheduleChunkFetch()
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
canvas.addEventListener('pointerleave', () => {
|
|
138
|
+
state.hoveredNodeId = ''
|
|
139
|
+
canvas.classList.remove('is-node-hover')
|
|
140
|
+
hideTooltip()
|
|
141
|
+
updateGraphOverlays()
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
canvas.addEventListener('pointercancel', (event) => {
|
|
145
|
+
resetPointerState(event.pointerId)
|
|
146
|
+
hideTooltip()
|
|
147
|
+
updateGraphOverlays()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
canvas.addEventListener('lostpointercapture', () => {
|
|
151
|
+
resetPointerState()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Listeners globais (mini mapa e teclado) só podem ser ligados uma vez —
|
|
155
|
+
// setupInput pode ser re-executado ao trocar o canvas no fallback.
|
|
156
|
+
if (!inputGlobalsBound) {
|
|
157
|
+
elements.miniMap.addEventListener('click', (event) => {
|
|
158
|
+
if (!state.miniMapView) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const rect = elements.miniMap.getBoundingClientRect()
|
|
162
|
+
const x = event.clientX - rect.left
|
|
163
|
+
const y = event.clientY - rect.top
|
|
164
|
+
const worldX = state.miniMapView.minX + (x - state.miniMapView.offsetX) / state.miniMapView.scale
|
|
165
|
+
const worldY = state.miniMapView.minY + (y - state.miniMapView.offsetY) / state.miniMapView.scale
|
|
166
|
+
state.camera.x = state.viewport.width / 2 - worldX * state.camera.scale
|
|
167
|
+
state.camera.y = state.viewport.height / 2 - worldY * state.camera.scale
|
|
168
|
+
updateWorkerCamera()
|
|
169
|
+
scheduleChunkFetch()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
window.addEventListener('keydown', (event) => {
|
|
173
|
+
if (event.key === 'Escape' && !elements.uploadDialog.hidden) {
|
|
174
|
+
closeUploadDialog()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
if (event.key === 'Escape' && !elements.contentDialog.hidden) {
|
|
178
|
+
closeContentDialog()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (event.key === '+') {
|
|
182
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 1.06)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if (event.key === '-') {
|
|
186
|
+
zoomAtPoint(state.viewport.width / 2, state.viewport.height / 2, 0.944)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
if (event.key === '0') {
|
|
190
|
+
scheduleChunkFetch({ fit: true })
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
inputGlobalsBound = true
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
canvas.addEventListener('dblclick', (event) => {
|
|
198
|
+
const pointer = resolvePointer(event)
|
|
199
|
+
zoomAtPoint(pointer.x, pointer.y, 1.065)
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
export const createNodeDetailsJs = () => `
|
|
2
|
+
const list = (items) => {
|
|
3
|
+
const rows = normalizeList(items)
|
|
4
|
+
if (rows.length === 0) {
|
|
5
|
+
return '<li><small>No links found.</small></li>'
|
|
6
|
+
}
|
|
7
|
+
return rows
|
|
8
|
+
.map((item) => {
|
|
9
|
+
const title = typeof item?.title === 'string' ? item.title : 'Untitled'
|
|
10
|
+
const id = typeof item?.id === 'string' ? item.id : ''
|
|
11
|
+
const path = typeof item?.path === 'string' ? item.path : ''
|
|
12
|
+
const meta = item?.weight ? ' · weight ' + escapeHtml(item.weight) + ' · ' + escapeHtml(item.priority || 'normal') : ''
|
|
13
|
+
return '<li>' +
|
|
14
|
+
(id ? '<button type="button" data-node-id="' + escapeHtml(id) + '">' + escapeHtml(title) + '</button>' : escapeHtml(title)) +
|
|
15
|
+
'<small>' + escapeHtml(path) + meta + '</small>' +
|
|
16
|
+
'</li>'
|
|
17
|
+
})
|
|
18
|
+
.join('')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const buildFacts = (node, outgoingCount, incomingCount) => {
|
|
22
|
+
const content = typeof node?.content === 'string' ? node.content : ''
|
|
23
|
+
const words = content.trim().length > 0 ? content.trim().split(/\\s+/).length : 0
|
|
24
|
+
return [
|
|
25
|
+
{ label: 'Agent', value: typeof node?.agentId === 'string' && node.agentId ? node.agentId : 'shared' },
|
|
26
|
+
{ label: 'Words', value: String(words) },
|
|
27
|
+
{ label: 'Chars', value: String(content.length) },
|
|
28
|
+
{ label: 'Outgoing', value: String(outgoingCount) },
|
|
29
|
+
{ label: 'Backlinks', value: String(incomingCount) }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const listFacts = (facts) => facts
|
|
34
|
+
.map((fact) => '<li><strong>' + escapeHtml(fact.label) + ':</strong> <small>' + escapeHtml(fact.value) + '</small></li>')
|
|
35
|
+
.join('')
|
|
36
|
+
|
|
37
|
+
const listContextLinks = (links) => {
|
|
38
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
39
|
+
return '<li><small>No context links found.</small></li>'
|
|
40
|
+
}
|
|
41
|
+
return links
|
|
42
|
+
.map((link) => '<li><span>' + escapeHtml(link.title) + '</span><small>' + escapeHtml(link.priority || 'normal') + '</small></li>')
|
|
43
|
+
.join('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const nodeContextLinks = (node, outgoing) => {
|
|
47
|
+
const titles = Array.isArray(node?.contextLinks) ? node.contextLinks : []
|
|
48
|
+
const outgoingByTitle = new Map(normalizeList(outgoing).map((link) => [String(link.title || '').toLowerCase(), link]))
|
|
49
|
+
|
|
50
|
+
return titles
|
|
51
|
+
.map((title) => {
|
|
52
|
+
const match = outgoingByTitle.get(String(title).toLowerCase())
|
|
53
|
+
return {
|
|
54
|
+
title,
|
|
55
|
+
priority: match?.priority || 'normal'
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.filter((link) => typeof link.title === 'string' && link.title.trim().length > 0)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const linkedNodes = (node) => {
|
|
62
|
+
const nodeById = new Map((state.chunk.nodes || []).map((item) => [item[0], item]))
|
|
63
|
+
const edges = normalizeList(state.chunk.edges)
|
|
64
|
+
|
|
65
|
+
const outgoing = []
|
|
66
|
+
const incoming = []
|
|
67
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
68
|
+
const edge = edges[index]
|
|
69
|
+
if (edge[0] === node.id) {
|
|
70
|
+
const target = nodeById.get(edge[1])
|
|
71
|
+
if (target) {
|
|
72
|
+
outgoing.push({ id: target[0], title: target[1], path: target[4] || '', weight: edge[2], priority: edge[3] })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (edge[1] === node.id) {
|
|
76
|
+
const source = nodeById.get(edge[0])
|
|
77
|
+
if (source) {
|
|
78
|
+
incoming.push({ id: source[0], title: source[1], path: source[4] || '', weight: edge[2], priority: edge[3] })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { outgoing, incoming }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const openContentDialog = () => {
|
|
87
|
+
elements.contentDialog.hidden = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const closeContentDialog = () => {
|
|
91
|
+
elements.contentDialog.hidden = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const selectedNode = () => {
|
|
95
|
+
if (!state.selectedNodeId) {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const packed = nodeByIdFromChunk().get(state.selectedNodeId)
|
|
100
|
+
if (!packed) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
id: packed[0],
|
|
106
|
+
title: packed[1],
|
|
107
|
+
path: packed[4] || ''
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const copySelectedWikiLink = async () => {
|
|
112
|
+
const node = selectedNode()
|
|
113
|
+
if (!node) {
|
|
114
|
+
elements.contentActionStatus.textContent = 'Select a note first.'
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const value = '[[' + node.title + ']]'
|
|
119
|
+
try {
|
|
120
|
+
await navigator.clipboard.writeText(value)
|
|
121
|
+
elements.contentActionStatus.textContent = 'Copied ' + value
|
|
122
|
+
} catch {
|
|
123
|
+
elements.contentActionStatus.textContent = value
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const loadSelectedLinkSuggestions = async () => {
|
|
128
|
+
const content = elements.contentBody.textContent || ''
|
|
129
|
+
if (!content.trim()) {
|
|
130
|
+
elements.contentActionStatus.textContent = 'Selected note has no content.'
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
elements.contentActionStatus.textContent = 'Loading suggestions...'
|
|
135
|
+
const response = await fetch('/api/suggest-links?limit=5&content=' + encodeURIComponent(content.slice(0, 2000)) + scopeQuery('&'))
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error('Failed to load link suggestions')
|
|
138
|
+
}
|
|
139
|
+
const payload = await response.json()
|
|
140
|
+
const suggestions = Array.isArray(payload.suggestions) ? payload.suggestions : []
|
|
141
|
+
elements.contentLinkSuggestions.innerHTML = suggestions.length > 0
|
|
142
|
+
? suggestions.map((item) => '<li><button type="button" data-title="' + escapeHtml(item.title) + '">[[' + escapeHtml(item.title) + ']]</button></li>').join('')
|
|
143
|
+
: '<li>No strong suggestions</li>'
|
|
144
|
+
elements.contentActionStatus.textContent = suggestions.length > 0 ? 'Suggested Context Links' : 'No strong suggestions found.'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const loadNodeDetails = async (nodeId) => {
|
|
148
|
+
if (!nodeId) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const response = await fetch('/api/graph-node?id=' + encodeURIComponent(nodeId) + scopeQuery('&'))
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new Error('Failed to load graph node details')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const payload = await response.json()
|
|
158
|
+
if (!payload || typeof payload !== 'object' || !payload.node) {
|
|
159
|
+
throw new Error('Invalid graph node payload')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const node = payload.node
|
|
163
|
+
state.selectedNodeId = node.id
|
|
164
|
+
setFocusedNodeIds(linkedNodeIds(node.id))
|
|
165
|
+
|
|
166
|
+
if (state.renderWorker && state.workerReady) {
|
|
167
|
+
state.renderWorker.postMessage({ type: 'select', id: node.id })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
elements.contentTitle.textContent = node.title || 'Untitled'
|
|
171
|
+
elements.contentPath.textContent = node.path || ''
|
|
172
|
+
|
|
173
|
+
const tags = Array.isArray(node.tags) ? node.tags : []
|
|
174
|
+
elements.contentTags.innerHTML = tags.length > 0
|
|
175
|
+
? tags.map((tag) => '<span>' + escapeHtml(tag) + '</span>').join('')
|
|
176
|
+
: '<span>No tags</span>'
|
|
177
|
+
|
|
178
|
+
const related = linkedNodes(node)
|
|
179
|
+
const contextLinks = nodeContextLinks(node, related.outgoing)
|
|
180
|
+
const facts = buildFacts(node, related.outgoing.length, related.incoming.length)
|
|
181
|
+
elements.contentFacts.innerHTML = listFacts(facts)
|
|
182
|
+
elements.contentContextLinks.innerHTML = listContextLinks(contextLinks)
|
|
183
|
+
elements.contentOutgoing.innerHTML = list(related.outgoing)
|
|
184
|
+
elements.contentIncoming.innerHTML = list(related.incoming)
|
|
185
|
+
elements.contentBody.textContent = typeof node.content === 'string' ? node.content : ''
|
|
186
|
+
elements.contentActionStatus.textContent = ''
|
|
187
|
+
elements.contentLinkSuggestions.innerHTML = ''
|
|
188
|
+
|
|
189
|
+
openContentDialog()
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
export const createRenderingJs = () => `
|
|
2
|
+
const drawFallback = () => {
|
|
3
|
+
if (state.rendererMode !== 'fallback') {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
ctx2dFallback = ctx2dFallback ?? canvas.getContext('2d')
|
|
7
|
+
if (!ctx2dFallback) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
const width = state.viewport.width
|
|
11
|
+
const height = state.viewport.height
|
|
12
|
+
const ratio = state.viewport.ratio
|
|
13
|
+
canvas.width = Math.floor(width * ratio)
|
|
14
|
+
canvas.height = Math.floor(height * ratio)
|
|
15
|
+
ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
16
|
+
ctx2dFallback.fillStyle = '#08131d'
|
|
17
|
+
ctx2dFallback.fillRect(0, 0, width, height)
|
|
18
|
+
|
|
19
|
+
const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
|
|
20
|
+
const edges = Array.isArray(state.chunk.edges) ? state.chunk.edges : []
|
|
21
|
+
const nodeById = new Map()
|
|
22
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
23
|
+
nodeById.set(nodes[i][0], nodes[i])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx2dFallback.strokeStyle = 'rgba(151,181,212,0.18)'
|
|
27
|
+
ctx2dFallback.lineWidth = 1
|
|
28
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
29
|
+
const edge = edges[i]
|
|
30
|
+
const source = nodeById.get(edge[0])
|
|
31
|
+
const target = nodeById.get(edge[1])
|
|
32
|
+
if (!source || !target) continue
|
|
33
|
+
const from = worldToScreen(source[2], source[3])
|
|
34
|
+
const to = worldToScreen(target[2], target[3])
|
|
35
|
+
ctx2dFallback.beginPath()
|
|
36
|
+
ctx2dFallback.moveTo(from.x, from.y)
|
|
37
|
+
ctx2dFallback.lineTo(to.x, to.y)
|
|
38
|
+
ctx2dFallback.stroke()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
42
|
+
const node = nodes[i]
|
|
43
|
+
const p = worldToScreen(node[2], node[3])
|
|
44
|
+
const selected = state.selectedNodeId === node[0]
|
|
45
|
+
const color = segmentColor(node[5] || node[4] || node[1])
|
|
46
|
+
const radius = Math.max(3.2, Math.min(16.5, 5 + node[7] * 0.65))
|
|
47
|
+
|
|
48
|
+
ctx2dFallback.beginPath()
|
|
49
|
+
ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
|
|
50
|
+
ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
|
51
|
+
ctx2dFallback.fill()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ctx2dFallback.fillStyle = '#97a9bd'
|
|
55
|
+
ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
|
|
56
|
+
ctx2dFallback.textAlign = 'center'
|
|
57
|
+
ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const updateTotals = () => {
|
|
61
|
+
elements.nodeCount.textContent = String(state.totals.nodes)
|
|
62
|
+
elements.edgeCount.textContent = String(state.totals.edges)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const updateWorkerCamera = () => {
|
|
66
|
+
updateGraphOverlays()
|
|
67
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (state.cameraSyncScheduled) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
state.cameraSyncScheduled = true
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
state.cameraSyncScheduled = false
|
|
76
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
state.renderWorker.postMessage({
|
|
80
|
+
type: 'camera',
|
|
81
|
+
camera: state.camera
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const updateWorkerSize = () => {
|
|
87
|
+
updateGraphOverlays()
|
|
88
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
state.renderWorker.postMessage({
|
|
92
|
+
type: 'resize',
|
|
93
|
+
width: state.viewport.width,
|
|
94
|
+
height: state.viewport.height,
|
|
95
|
+
devicePixelRatio: state.viewport.ratio
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalizeList = (items) => Array.isArray(items) ? items : []
|
|
100
|
+
|
|
101
|
+
const applyManualNodePositions = (nodes) => normalizeList(nodes).map((node) => {
|
|
102
|
+
const id = typeof node?.[0] === 'string' ? node[0] : ''
|
|
103
|
+
const position = id ? state.nodePositions.get(id) : null
|
|
104
|
+
if (!position || !Number.isFinite(position.x) || !Number.isFinite(position.y)) {
|
|
105
|
+
return node
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const next = [...node]
|
|
109
|
+
next[2] = position.x
|
|
110
|
+
next[3] = position.y
|
|
111
|
+
return next
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const updateNodePositionInChunk = (nodeId, x, y) => {
|
|
115
|
+
if (!nodeId || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
state.chunk = {
|
|
120
|
+
...state.chunk,
|
|
121
|
+
nodes: normalizeList(state.chunk.nodes).map((node) => {
|
|
122
|
+
if (node?.[0] !== nodeId) {
|
|
123
|
+
return node
|
|
124
|
+
}
|
|
125
|
+
const next = [...node]
|
|
126
|
+
next[2] = x
|
|
127
|
+
next[3] = y
|
|
128
|
+
return next
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
state.spatialIndex.key = ''
|
|
132
|
+
|
|
133
|
+
if (state.renderWorker && state.workerReady) {
|
|
134
|
+
state.renderWorker.postMessage({ type: 'move-node', id: nodeId, x, y })
|
|
135
|
+
}
|
|
136
|
+
updateGraphOverlays()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const focusNodeInViewport = (nodeId, nextScale = null) => {
|
|
140
|
+
const node = nodeByIdFromChunk().get(nodeId)
|
|
141
|
+
if (!node) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const x = Number(node[2])
|
|
146
|
+
const y = Number(node[3])
|
|
147
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Number.isFinite(nextScale)) {
|
|
152
|
+
state.camera.scale = clampScale(Number(nextScale))
|
|
153
|
+
}
|
|
154
|
+
state.camera.x = state.viewport.width / 2 - x * state.camera.scale
|
|
155
|
+
state.camera.y = state.viewport.height / 2 - y * state.camera.scale
|
|
156
|
+
updateWorkerCamera()
|
|
157
|
+
scheduleChunkFetch()
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const showTooltip = (node, pointer) => {
|
|
162
|
+
if (!elements.tooltip || !node) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
elements.tooltip.hidden = false
|
|
167
|
+
elements.tooltip.innerHTML =
|
|
168
|
+
'<strong>' + escapeHtml(node[1] || node[0]) + '</strong>' +
|
|
169
|
+
'<small>' + escapeHtml(node[4] || node[5] || '') + '</small>'
|
|
170
|
+
elements.tooltip.style.left = Math.min(state.viewport.width - 24, pointer.x + 14) + 'px'
|
|
171
|
+
elements.tooltip.style.top = Math.min(state.viewport.height - 24, pointer.y + 14) + 'px'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const hideTooltip = () => {
|
|
175
|
+
if (elements.tooltip) {
|
|
176
|
+
elements.tooltip.hidden = true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const labelCandidates = () => {
|
|
181
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
182
|
+
const visible = nodes.filter((node) => {
|
|
183
|
+
const x = Number(node?.[2])
|
|
184
|
+
const y = Number(node?.[3])
|
|
185
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false
|
|
186
|
+
const point = worldToScreen(x, y)
|
|
187
|
+
return point.x >= -80 && point.x <= state.viewport.width + 80 && point.y >= -80 && point.y <= state.viewport.height + 80
|
|
188
|
+
})
|
|
189
|
+
const shouldShowMany = state.camera.scale >= 0.72 || visible.length <= 120
|
|
190
|
+
const focused = state.focusedNodeIds
|
|
191
|
+
|
|
192
|
+
return visible
|
|
193
|
+
.filter((node) => shouldShowMany || focused.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId || Number(node?.[7]) > 5.5)
|
|
194
|
+
.sort((left, right) => {
|
|
195
|
+
const leftFocused = focused.has(left[0]) || left[0] === state.hoveredNodeId || left[0] === state.selectedNodeId ? 1 : 0
|
|
196
|
+
const rightFocused = focused.has(right[0]) || right[0] === state.hoveredNodeId || right[0] === state.selectedNodeId ? 1 : 0
|
|
197
|
+
if (rightFocused !== leftFocused) return rightFocused - leftFocused
|
|
198
|
+
return Number(right?.[7] ?? 0) - Number(left?.[7] ?? 0)
|
|
199
|
+
})
|
|
200
|
+
.slice(0, state.camera.scale >= 0.72 ? 160 : 48)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const drawLabels = () => {
|
|
204
|
+
if (!elements.labels) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
elements.labels.innerHTML = labelCandidates().map((node) => {
|
|
209
|
+
const point = worldToScreen(Number(node[2]), Number(node[3]))
|
|
210
|
+
const focused = state.focusedNodeIds.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId
|
|
211
|
+
return '<span class="graph-label' + (focused ? ' is-focused' : '') + '" style="left:' +
|
|
212
|
+
point.x.toFixed(1) + 'px;top:' + point.y.toFixed(1) + 'px">' + escapeHtml(node[1] || node[0]) + '</span>'
|
|
213
|
+
}).join('')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const drawMiniMap = () => {
|
|
217
|
+
const miniMap = elements.miniMap
|
|
218
|
+
if (!(miniMap instanceof HTMLCanvasElement)) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
222
|
+
const ctx = miniMap.getContext('2d')
|
|
223
|
+
if (!ctx || nodes.length === 0) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const ratio = window.devicePixelRatio || 1
|
|
228
|
+
const width = miniMap.clientWidth || 180
|
|
229
|
+
const height = miniMap.clientHeight || 120
|
|
230
|
+
miniMap.width = Math.floor(width * ratio)
|
|
231
|
+
miniMap.height = Math.floor(height * ratio)
|
|
232
|
+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
233
|
+
ctx.clearRect(0, 0, width, height)
|
|
234
|
+
ctx.fillStyle = 'rgba(8, 19, 29, 0.88)'
|
|
235
|
+
ctx.fillRect(0, 0, width, height)
|
|
236
|
+
|
|
237
|
+
const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
|
|
238
|
+
const ys = nodes.map((node) => Number(node[3])).filter(Number.isFinite)
|
|
239
|
+
const minX = Math.min(...xs)
|
|
240
|
+
const maxX = Math.max(...xs)
|
|
241
|
+
const minY = Math.min(...ys)
|
|
242
|
+
const maxY = Math.max(...ys)
|
|
243
|
+
const graphWidth = Math.max(1, maxX - minX)
|
|
244
|
+
const graphHeight = Math.max(1, maxY - minY)
|
|
245
|
+
const scale = Math.min((width - 18) / graphWidth, (height - 18) / graphHeight)
|
|
246
|
+
const offsetX = (width - graphWidth * scale) / 2
|
|
247
|
+
const offsetY = (height - graphHeight * scale) / 2
|
|
248
|
+
const toMini = (x, y) => ({
|
|
249
|
+
x: offsetX + (x - minX) * scale,
|
|
250
|
+
y: offsetY + (y - minY) * scale
|
|
251
|
+
})
|
|
252
|
+
state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
|
|
253
|
+
|
|
254
|
+
ctx.fillStyle = 'rgba(90, 168, 255, 0.62)'
|
|
255
|
+
nodes.forEach((node) => {
|
|
256
|
+
const point = toMini(Number(node[2]), Number(node[3]))
|
|
257
|
+
ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const worldTopLeft = screenToWorld(0, 0)
|
|
261
|
+
const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
|
|
262
|
+
const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
|
|
263
|
+
const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
|
|
264
|
+
ctx.strokeStyle = 'rgba(90, 168, 255, 0.86)'
|
|
265
|
+
ctx.lineWidth = 1
|
|
266
|
+
ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
|
|
270
|
+
|
|
271
|
+
const updateGraphOverlays = () => {
|
|
272
|
+
if (state.overlayScheduled) {
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
state.overlayScheduled = true
|
|
276
|
+
requestAnimationFrame(() => {
|
|
277
|
+
state.overlayScheduled = false
|
|
278
|
+
if (shouldDeferGraphOverlays()) {
|
|
279
|
+
elements.labels?.classList.add('is-stale')
|
|
280
|
+
if (!state.overlayIdleTimer) {
|
|
281
|
+
state.overlayIdleTimer = setTimeout(() => {
|
|
282
|
+
state.overlayIdleTimer = null
|
|
283
|
+
updateGraphOverlays()
|
|
284
|
+
}, 170)
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
elements.labels?.classList.remove('is-stale')
|
|
289
|
+
drawLabels()
|
|
290
|
+
if (state.miniMapDirty) {
|
|
291
|
+
drawMiniMap()
|
|
292
|
+
state.miniMapDirty = false
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
`;
|