@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,114 @@
|
|
|
1
|
+
export const createScopeThemeJs = () => `
|
|
2
|
+
const initialAgentFromUrl = (() => {
|
|
3
|
+
try {
|
|
4
|
+
const raw = new URL(window.location.href).searchParams.get('agent')
|
|
5
|
+
const value = raw?.trim() ?? ''
|
|
6
|
+
return value.length > 0 ? value : ''
|
|
7
|
+
} catch {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
})()
|
|
11
|
+
|
|
12
|
+
const initialContextFromUrl = (() => {
|
|
13
|
+
try {
|
|
14
|
+
const raw = new URL(window.location.href).searchParams.get('context')
|
|
15
|
+
const value = raw?.trim() ?? ''
|
|
16
|
+
return value.length > 0 ? value : ''
|
|
17
|
+
} catch {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
})()
|
|
21
|
+
|
|
22
|
+
const scopeQuery = (separator = '?') => {
|
|
23
|
+
const params = new URLSearchParams()
|
|
24
|
+
if (state.agentId) {
|
|
25
|
+
params.set('agent', state.agentId)
|
|
26
|
+
}
|
|
27
|
+
if (state.contextId) {
|
|
28
|
+
params.set('context', state.contextId)
|
|
29
|
+
}
|
|
30
|
+
const query = params.toString()
|
|
31
|
+
|
|
32
|
+
return query ? separator + query : ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parseColor = (hex) => {
|
|
36
|
+
const normalized = String(hex || '#ffffff').replace('#', '')
|
|
37
|
+
const expanded = normalized.length === 3
|
|
38
|
+
? normalized.split('').map((char) => char + char).join('')
|
|
39
|
+
: normalized.padEnd(6, 'f')
|
|
40
|
+
const value = Number.parseInt(expanded, 16)
|
|
41
|
+
return [
|
|
42
|
+
((value >> 16) & 255) / 255,
|
|
43
|
+
((value >> 8) & 255) / 255,
|
|
44
|
+
(value & 255) / 255,
|
|
45
|
+
1
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const graphTheme = {
|
|
50
|
+
node: parseColor('#5aa8ff'),
|
|
51
|
+
nodeCluster: parseColor('#3f7fbd'),
|
|
52
|
+
nodeHighlight: parseColor('#ffcb67'),
|
|
53
|
+
nodeSelected: parseColor('#edf4ff'),
|
|
54
|
+
nodePalette: [
|
|
55
|
+
parseColor('#5aa8ff'),
|
|
56
|
+
parseColor('#5ecf92'),
|
|
57
|
+
parseColor('#ffb65c'),
|
|
58
|
+
parseColor('#ff7dac'),
|
|
59
|
+
parseColor('#a88fff'),
|
|
60
|
+
parseColor('#59d0dd'),
|
|
61
|
+
parseColor('#ff8f6a'),
|
|
62
|
+
parseColor('#a4b3c3'),
|
|
63
|
+
parseColor('#c9945f'),
|
|
64
|
+
parseColor('#7cb6ff')
|
|
65
|
+
],
|
|
66
|
+
edge: [0.59, 0.71, 0.83, 0.14],
|
|
67
|
+
edgeHeavy: [0.59, 0.71, 0.83, 0.3],
|
|
68
|
+
clear: parseColor('#08131d')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
|
|
72
|
+
|
|
73
|
+
const segmentColorIndex = (segment) => {
|
|
74
|
+
const value = String(segment || '')
|
|
75
|
+
let hash = 0
|
|
76
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
77
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
|
|
78
|
+
}
|
|
79
|
+
return Math.abs(hash) % segmentPalette.length
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
|
|
83
|
+
const nodeKind = (node) => node?.[6] === 'cluster' ? 'cluster' : 'node'
|
|
84
|
+
const isRealGraphNode = (node) => nodeKind(node) === 'node'
|
|
85
|
+
|
|
86
|
+
const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
|
|
87
|
+
|
|
88
|
+
const getZoomNodeBudget = () => {
|
|
89
|
+
const scale = state.camera.scale
|
|
90
|
+
if (scale < 0.06) return 900
|
|
91
|
+
if (scale < 0.12) return 1600
|
|
92
|
+
if (scale < 0.24) return 2600
|
|
93
|
+
if (scale < 0.7) return 4000
|
|
94
|
+
return 6000
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const getZoomEdgeBudget = () => {
|
|
98
|
+
const scale = state.camera.scale
|
|
99
|
+
if (scale < 0.06) return 2000
|
|
100
|
+
if (scale < 0.12) return 4800
|
|
101
|
+
if (scale < 0.24) return 9000
|
|
102
|
+
if (scale < 0.7) return 15000
|
|
103
|
+
return 26000
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const zoomDetailBand = () => {
|
|
107
|
+
const scale = state.camera.scale
|
|
108
|
+
if (scale < 0.06) return 'far'
|
|
109
|
+
if (scale < 0.12) return 'wide'
|
|
110
|
+
if (scale < 0.24) return 'mid'
|
|
111
|
+
if (scale < 0.7) return 'near'
|
|
112
|
+
return 'detail'
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const createSpatialJs = () => `
|
|
2
|
+
const graphStreamRequestKey = ({ x, y, w, h }) => {
|
|
3
|
+
const grid = Math.max(80, Math.min(720, Math.max(w, h) / 6))
|
|
4
|
+
return [
|
|
5
|
+
state.agentId || '*',
|
|
6
|
+
state.contextId || '*',
|
|
7
|
+
zoomDetailBand(),
|
|
8
|
+
getZoomNodeBudget(),
|
|
9
|
+
getZoomEdgeBudget(),
|
|
10
|
+
Math.round(x / grid),
|
|
11
|
+
Math.round(y / grid),
|
|
12
|
+
Math.round(w / grid),
|
|
13
|
+
Math.round(h / grid)
|
|
14
|
+
].join(':')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const screenToWorld = (screenX, screenY) => ({
|
|
18
|
+
x: (screenX - state.camera.x) / state.camera.scale,
|
|
19
|
+
y: (screenY - state.camera.y) / state.camera.scale
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const worldToScreen = (x, y) => ({
|
|
23
|
+
x: x * state.camera.scale + state.camera.x,
|
|
24
|
+
y: y * state.camera.scale + state.camera.y
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const spatialIndexKey = () => [
|
|
28
|
+
state.graphSignature,
|
|
29
|
+
state.camera.x.toFixed(1),
|
|
30
|
+
state.camera.y.toFixed(1),
|
|
31
|
+
state.camera.scale.toFixed(4),
|
|
32
|
+
normalizeList(state.chunk.nodes).length
|
|
33
|
+
].join(':')
|
|
34
|
+
|
|
35
|
+
const rebuildSpatialIndex = () => {
|
|
36
|
+
const key = spatialIndexKey()
|
|
37
|
+
if (state.spatialIndex.key === key) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cellSize = 44
|
|
42
|
+
const cells = new Map()
|
|
43
|
+
normalizeList(state.chunk.nodes).forEach((node) => {
|
|
44
|
+
const id = typeof node?.[0] === 'string' ? node[0] : ''
|
|
45
|
+
if (!id) return
|
|
46
|
+
const x = Number(node?.[2])
|
|
47
|
+
const y = Number(node?.[3])
|
|
48
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
|
49
|
+
const point = worldToScreen(x, y)
|
|
50
|
+
const cellX = Math.floor(point.x / cellSize)
|
|
51
|
+
const cellY = Math.floor(point.y / cellSize)
|
|
52
|
+
const key = cellX + ',' + cellY
|
|
53
|
+
const bucket = cells.get(key)
|
|
54
|
+
if (bucket) {
|
|
55
|
+
bucket.push(node)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
cells.set(key, [node])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
state.spatialIndex = { key, cells }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const spatialCandidates = (screenX, screenY) => {
|
|
65
|
+
rebuildSpatialIndex()
|
|
66
|
+
const cellSize = 44
|
|
67
|
+
const cellX = Math.floor(screenX / cellSize)
|
|
68
|
+
const cellY = Math.floor(screenY / cellSize)
|
|
69
|
+
const nodes = []
|
|
70
|
+
|
|
71
|
+
for (let y = cellY - 1; y <= cellY + 1; y += 1) {
|
|
72
|
+
for (let x = cellX - 1; x <= cellX + 1; x += 1) {
|
|
73
|
+
nodes.push(...(state.spatialIndex.cells.get(x + ',' + y) ?? []))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nodes
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nodeByIdFromChunk = () => new Map(normalizeList(state.chunk.nodes).map((node) => [node[0], node]))
|
|
81
|
+
|
|
82
|
+
const linkedNodeIds = (nodeId) => {
|
|
83
|
+
const ids = new Set(nodeId ? [nodeId] : [])
|
|
84
|
+
normalizeList(state.chunk.edges).forEach((edge) => {
|
|
85
|
+
if (edge?.[0] === nodeId && typeof edge?.[1] === 'string') ids.add(edge[1])
|
|
86
|
+
if (edge?.[1] === nodeId && typeof edge?.[0] === 'string') ids.add(edge[0])
|
|
87
|
+
})
|
|
88
|
+
return ids
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const setFocusedNodeIds = (ids) => {
|
|
92
|
+
state.focusedNodeIds = ids
|
|
93
|
+
if (state.renderWorker && state.workerReady) {
|
|
94
|
+
state.renderWorker.postMessage({ type: 'focus', ids: Array.from(ids) })
|
|
95
|
+
}
|
|
96
|
+
updateGraphOverlays()
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export const createStorageJs = () => `
|
|
2
|
+
const escapeHtml = (value) => String(value)
|
|
3
|
+
.replaceAll('&', '&')
|
|
4
|
+
.replaceAll('<', '<')
|
|
5
|
+
.replaceAll('>', '>')
|
|
6
|
+
.replaceAll('"', '"')
|
|
7
|
+
.replaceAll("'", ''')
|
|
8
|
+
|
|
9
|
+
const readStoredAgent = () => {
|
|
10
|
+
try {
|
|
11
|
+
const value = window.localStorage.getItem(selectedAgentStorageKey)?.trim() ?? ''
|
|
12
|
+
return value.length > 0 ? value : ''
|
|
13
|
+
} catch {
|
|
14
|
+
return ''
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const writeStoredAgent = (agentId) => {
|
|
19
|
+
try {
|
|
20
|
+
if (!agentId) {
|
|
21
|
+
window.localStorage.removeItem(selectedAgentStorageKey)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
window.localStorage.setItem(selectedAgentStorageKey, agentId)
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const readStoredContext = () => {
|
|
29
|
+
try {
|
|
30
|
+
const value = window.localStorage.getItem(selectedContextStorageKey)?.trim() ?? ''
|
|
31
|
+
return value.length > 0 ? value : ''
|
|
32
|
+
} catch {
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const writeStoredContext = (contextId) => {
|
|
38
|
+
try {
|
|
39
|
+
if (!contextId) {
|
|
40
|
+
window.localStorage.removeItem(selectedContextStorageKey)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
window.localStorage.setItem(selectedContextStorageKey, contextId)
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nodePositionsStorageKey = () => [
|
|
48
|
+
nodePositionsStoragePrefix,
|
|
49
|
+
state.graphSignature || 'unknown',
|
|
50
|
+
state.agentId || 'all-agents',
|
|
51
|
+
state.contextId || 'all-contexts'
|
|
52
|
+
].join(':')
|
|
53
|
+
|
|
54
|
+
const readStoredNodePositions = () => {
|
|
55
|
+
try {
|
|
56
|
+
const raw = window.localStorage.getItem(nodePositionsStorageKey())
|
|
57
|
+
const parsed = raw ? JSON.parse(raw) : []
|
|
58
|
+
if (!Array.isArray(parsed)) {
|
|
59
|
+
return new Map()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Map(parsed.flatMap((entry) => {
|
|
63
|
+
const id = typeof entry?.[0] === 'string' ? entry[0] : ''
|
|
64
|
+
const x = Number(entry?.[1])
|
|
65
|
+
const y = Number(entry?.[2])
|
|
66
|
+
return id && Number.isFinite(x) && Number.isFinite(y) ? [[id, { x, y }]] : []
|
|
67
|
+
}))
|
|
68
|
+
} catch {
|
|
69
|
+
return new Map()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ensureNodePositionsLoaded = () => {
|
|
74
|
+
const storageKey = nodePositionsStorageKey()
|
|
75
|
+
if (!state.graphSignature || (state.nodePositionsSignature === state.graphSignature && state.nodePositionsScope === storageKey)) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
state.nodePositions = readStoredNodePositions()
|
|
80
|
+
state.nodePositionsSignature = state.graphSignature
|
|
81
|
+
state.nodePositionsScope = storageKey
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const writeStoredNodePositions = () => {
|
|
85
|
+
try {
|
|
86
|
+
if (!state.graphSignature) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const entries = Array.from(state.nodePositions.entries())
|
|
91
|
+
.filter((entry) => Number.isFinite(entry[1]?.x) && Number.isFinite(entry[1]?.y))
|
|
92
|
+
.map((entry) => [entry[0], entry[1].x, entry[1].y])
|
|
93
|
+
|
|
94
|
+
if (entries.length === 0) {
|
|
95
|
+
window.localStorage.removeItem(nodePositionsStorageKey())
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
window.localStorage.setItem(nodePositionsStorageKey(), JSON.stringify(entries))
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const clearStoredNodePositions = () => {
|
|
104
|
+
try {
|
|
105
|
+
if (state.graphSignature) {
|
|
106
|
+
window.localStorage.removeItem(nodePositionsStorageKey())
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
state.nodePositions = new Map()
|
|
110
|
+
state.nodePositionsSignature = state.graphSignature
|
|
111
|
+
state.nodePositionsScope = nodePositionsStorageKey()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const graphViewStateQuery = () => {
|
|
115
|
+
const params = new URLSearchParams({ signature: state.graphSignature })
|
|
116
|
+
if (state.agentId) {
|
|
117
|
+
params.set('agent', state.agentId)
|
|
118
|
+
}
|
|
119
|
+
if (state.contextId) {
|
|
120
|
+
params.set('context', state.contextId)
|
|
121
|
+
}
|
|
122
|
+
return params.toString()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const syncNodePositionsFromServer = async () => {
|
|
126
|
+
if (!state.graphSignature) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const scope = nodePositionsStorageKey()
|
|
130
|
+
if (state.serverNodePositionsScope === scope) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
state.serverNodePositionsScope = scope
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const response = await fetch('/api/graph-view-state?' + graphViewStateQuery())
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
const payload = await response.json()
|
|
141
|
+
const positions = Array.isArray(payload?.positions) ? payload.positions : []
|
|
142
|
+
if (positions.length === 0) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
state.nodePositions = new Map(positions.flatMap((position) => {
|
|
146
|
+
const id = typeof position?.id === 'string' ? position.id : ''
|
|
147
|
+
const x = Number(position?.x)
|
|
148
|
+
const y = Number(position?.y)
|
|
149
|
+
return id && Number.isFinite(x) && Number.isFinite(y) ? [[id, { x, y }]] : []
|
|
150
|
+
}))
|
|
151
|
+
writeStoredNodePositions()
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const persistNodePositionsToServer = () => {
|
|
156
|
+
if (!state.graphSignature) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const positions = Array.from(state.nodePositions.entries()).map(([id, position]) => ({
|
|
161
|
+
id,
|
|
162
|
+
x: position.x,
|
|
163
|
+
y: position.y
|
|
164
|
+
}))
|
|
165
|
+
|
|
166
|
+
fetch('/api/graph-view-state?' + graphViewStateQuery(), {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'content-type': 'application/json' },
|
|
169
|
+
body: JSON.stringify({ positions })
|
|
170
|
+
}).catch(() => {})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const clearNodePositionsOnServer = () => {
|
|
174
|
+
if (!state.graphSignature) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fetch('/api/graph-view-state?' + graphViewStateQuery(), { method: 'DELETE' }).catch(() => {})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const releaseSelectedNodePosition = () => {
|
|
182
|
+
if (!state.selectedNodeId || !state.nodePositions.has(state.selectedNodeId)) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
state.nodePositions.delete(state.selectedNodeId)
|
|
187
|
+
writeStoredNodePositions()
|
|
188
|
+
persistNodePositionsToServer()
|
|
189
|
+
scheduleChunkFetch({ fit: false })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const syncAgentInUrl = (agentId) => {
|
|
193
|
+
try {
|
|
194
|
+
const url = new URL(window.location.href)
|
|
195
|
+
if (agentId && agentId.trim().length > 0) {
|
|
196
|
+
url.searchParams.set('agent', agentId)
|
|
197
|
+
} else {
|
|
198
|
+
url.searchParams.delete('agent')
|
|
199
|
+
}
|
|
200
|
+
window.history.replaceState({}, '', url.toString())
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const syncContextInUrl = (contextId) => {
|
|
205
|
+
try {
|
|
206
|
+
const url = new URL(window.location.href)
|
|
207
|
+
if (contextId && contextId.trim().length > 0) {
|
|
208
|
+
url.searchParams.set('context', contextId)
|
|
209
|
+
} else {
|
|
210
|
+
url.searchParams.delete('context')
|
|
211
|
+
}
|
|
212
|
+
window.history.replaceState({}, '', url.toString())
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const createUploadJs = () => `
|
|
2
|
+
const openUploadDialog = () => {
|
|
3
|
+
elements.uploadDialog.hidden = false
|
|
4
|
+
elements.uploadStatus.textContent = ''
|
|
5
|
+
elements.uploadTitleInput.value = ''
|
|
6
|
+
elements.uploadFile.value = ''
|
|
7
|
+
elements.uploadAllowSensitive.checked = false
|
|
8
|
+
window.setTimeout(() => elements.uploadFile.focus(), 0)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const closeUploadDialog = () => {
|
|
12
|
+
elements.uploadDialog.hidden = true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const setUploadBusy = (busy) => {
|
|
16
|
+
elements.uploadSubmit.disabled = busy
|
|
17
|
+
elements.uploadFile.disabled = busy
|
|
18
|
+
elements.uploadTitleInput.disabled = busy
|
|
19
|
+
elements.uploadAllowSensitive.disabled = busy
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const uploadImportUrl = () => {
|
|
23
|
+
const params = new URLSearchParams()
|
|
24
|
+
if (state.agentId) {
|
|
25
|
+
params.set('agent', state.agentId)
|
|
26
|
+
}
|
|
27
|
+
const query = params.toString()
|
|
28
|
+
|
|
29
|
+
return '/api/import-file' + (query ? '?' + query : '')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const submitUpload = async (event) => {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
const file = elements.uploadFile.files?.[0]
|
|
35
|
+
if (!file) {
|
|
36
|
+
elements.uploadStatus.textContent = 'Choose a file to import.'
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const form = new FormData()
|
|
41
|
+
form.append('file', file)
|
|
42
|
+
const title = elements.uploadTitleInput.value.trim()
|
|
43
|
+
if (title) {
|
|
44
|
+
form.append('title', title)
|
|
45
|
+
}
|
|
46
|
+
if (elements.uploadAllowSensitive.checked) {
|
|
47
|
+
form.append('allowSensitive', 'true')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
setUploadBusy(true)
|
|
51
|
+
elements.uploadStatus.textContent = 'Importing...'
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(uploadImportUrl(), {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body: form
|
|
57
|
+
})
|
|
58
|
+
const payload = await response.json().catch(() => ({}))
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(payload?.error || 'Import failed')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
elements.uploadStatus.textContent = 'Imported "' + String(payload?.title || file.name) + '".'
|
|
64
|
+
await loadAgents()
|
|
65
|
+
await loadContexts()
|
|
66
|
+
scheduleChunkFetch({ fit: true })
|
|
67
|
+
window.setTimeout(closeUploadDialog, 700)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
elements.uploadStatus.textContent = error instanceof Error ? error.message : String(error)
|
|
70
|
+
} finally {
|
|
71
|
+
setUploadBusy(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const setupUploadDialog = () => {
|
|
76
|
+
elements.uploadOpen.addEventListener('click', openUploadDialog)
|
|
77
|
+
elements.uploadClose.addEventListener('click', closeUploadDialog)
|
|
78
|
+
elements.uploadForm.addEventListener('submit', (event) => {
|
|
79
|
+
submitUpload(event).catch((error) => {
|
|
80
|
+
elements.uploadStatus.textContent = error instanceof Error ? error.message : String(error)
|
|
81
|
+
setUploadBusy(false)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
elements.uploadDialog.addEventListener('click', (event) => {
|
|
85
|
+
if (event.target === elements.uploadDialog) {
|
|
86
|
+
closeUploadDialog()
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const createWorkerBootstrapJs = () => `
|
|
2
|
+
const mainThreadWebGlAvailable = () => {
|
|
3
|
+
try {
|
|
4
|
+
const probe = document.createElement('canvas')
|
|
5
|
+
const gl = probe.getContext('webgl2') || probe.getContext('webgl') || probe.getContext('experimental-webgl')
|
|
6
|
+
return Boolean(gl)
|
|
7
|
+
} catch (error) {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Uma vez que o controle do canvas é transferido para o worker (OffscreenCanvas),
|
|
13
|
+
// o elemento original não fornece mais nenhum contexto (2D ou WebGL). Quando o
|
|
14
|
+
// worker falha ao iniciar o WebGL, trocamos por um canvas novo para que o
|
|
15
|
+
// fallback 2D consiga desenhar o grafo.
|
|
16
|
+
const replaceGraphCanvas = () => {
|
|
17
|
+
const replacement = canvas.cloneNode(false)
|
|
18
|
+
if (canvas.parentNode) {
|
|
19
|
+
canvas.parentNode.replaceChild(replacement, canvas)
|
|
20
|
+
}
|
|
21
|
+
canvas = replacement
|
|
22
|
+
ctx2dFallback = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const enterFallbackMode = (rebindInput) => {
|
|
26
|
+
state.rendererMode = 'fallback'
|
|
27
|
+
if (rebindInput) {
|
|
28
|
+
setupInput()
|
|
29
|
+
}
|
|
30
|
+
setViewportFromCanvas()
|
|
31
|
+
drawFallback()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const setupRenderWorker = () => {
|
|
35
|
+
const hasWorker = typeof Worker !== 'undefined'
|
|
36
|
+
const canTransfer = typeof canvas.transferControlToOffscreen === 'function'
|
|
37
|
+
|
|
38
|
+
if (!hasWorker || !canTransfer || !mainThreadWebGlAvailable()) {
|
|
39
|
+
enterFallbackMode(false)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const offscreen = canvas.transferControlToOffscreen()
|
|
45
|
+
const worker = new Worker('/render-worker.js')
|
|
46
|
+
state.renderWorker = worker
|
|
47
|
+
|
|
48
|
+
worker.onmessage = (event) => {
|
|
49
|
+
const payload = event.data
|
|
50
|
+
if (!payload || typeof payload !== 'object') {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (payload.type === 'ready') {
|
|
55
|
+
state.workerReady = true
|
|
56
|
+
scheduleChunkFetch({ fit: true })
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (payload.type === 'pick-result') {
|
|
61
|
+
if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
|
|
62
|
+
handlePickedNode(payload.node)
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (payload.type === 'frame-stats') {
|
|
68
|
+
state.lastVisibleNodes = Number.isFinite(payload.visibleNodes) ? payload.visibleNodes : state.lastVisibleNodes
|
|
69
|
+
state.lastVisibleEdges = Number.isFinite(payload.visibleEdges) ? payload.visibleEdges : state.lastVisibleEdges
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (payload.type === 'fatal') {
|
|
74
|
+
console.error(payload.message)
|
|
75
|
+
state.workerReady = false
|
|
76
|
+
if (state.renderWorker) {
|
|
77
|
+
state.renderWorker.terminate()
|
|
78
|
+
}
|
|
79
|
+
state.renderWorker = null
|
|
80
|
+
// O canvas já foi transferido para o worker: troca por um novo e
|
|
81
|
+
// religa os eventos de interação antes de cair no fallback 2D.
|
|
82
|
+
replaceGraphCanvas()
|
|
83
|
+
enterFallbackMode(true)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
worker.postMessage({
|
|
88
|
+
type: 'init',
|
|
89
|
+
canvas: offscreen,
|
|
90
|
+
width: state.viewport.width,
|
|
91
|
+
height: state.viewport.height,
|
|
92
|
+
devicePixelRatio: state.viewport.ratio,
|
|
93
|
+
camera: state.camera,
|
|
94
|
+
theme: graphTheme
|
|
95
|
+
}, [offscreen])
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(error)
|
|
98
|
+
replaceGraphCanvas()
|
|
99
|
+
enterFallbackMode(false)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const wireNodeLinkClicks = () => {
|
|
104
|
+
const dialog = elements.contentDialog
|
|
105
|
+
dialog.addEventListener('click', (event) => {
|
|
106
|
+
const target = event.target
|
|
107
|
+
if (!(target instanceof HTMLElement)) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const button = target.closest('button[data-node-id]')
|
|
112
|
+
if (!button) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const id = button.getAttribute('data-node-id') || ''
|
|
117
|
+
if (id) {
|
|
118
|
+
loadNodeDetails(id).catch((error) => console.error(error))
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const bootstrap = async () => {
|
|
124
|
+
setViewportFromCanvas()
|
|
125
|
+
setupRenderWorker()
|
|
126
|
+
setupInput()
|
|
127
|
+
setupControls()
|
|
128
|
+
setupUploadDialog()
|
|
129
|
+
setupContextControl()
|
|
130
|
+
wireNodeLinkClicks()
|
|
131
|
+
|
|
132
|
+
window.addEventListener('resize', () => {
|
|
133
|
+
setViewportFromCanvas()
|
|
134
|
+
scheduleChunkFetch()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await loadAgents()
|
|
138
|
+
await loadContexts()
|
|
139
|
+
updateTotals()
|
|
140
|
+
|
|
141
|
+
scheduleChunkFetch({ fit: true })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
bootstrap().catch((error) => {
|
|
145
|
+
console.error(error)
|
|
146
|
+
})
|
|
147
|
+
`;
|