total-recall-brain 3.1.2 → 3.2.2
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/extension/background.js +99 -0
- package/extension/content-script.css +16 -0
- package/extension/content-script.js +134 -0
- package/extension/icons/README.md +27 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/lib/brain-client.js +47 -0
- package/extension/manifest.json +39 -0
- package/extension/options/options.css +218 -0
- package/extension/options/options.html +72 -0
- package/extension/options/options.js +99 -0
- package/extension/popup/popup.css +221 -0
- package/extension/popup/popup.html +44 -0
- package/extension/popup/popup.js +107 -0
- package/extension/sidepanel/sidepanel.css +340 -0
- package/extension/sidepanel/sidepanel.html +38 -0
- package/extension/sidepanel/sidepanel.js +229 -0
- package/frontend/README.md +73 -0
- package/frontend/index.html +17 -0
- package/frontend/package.json +31 -0
- package/frontend/src/App.css +2 -0
- package/frontend/src/App.tsx +390 -0
- package/frontend/src/api.ts +688 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/BrainSelector.tsx +272 -0
- package/frontend/src/components/DaemonLogsTab.tsx +54 -0
- package/frontend/src/components/Graph3D.tsx +850 -0
- package/frontend/src/components/MarkdownUtils.tsx +99 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
- package/frontend/src/components/TaskQueueTab.tsx +185 -0
- package/frontend/src/index.css +1589 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/pages/ApiKeysPage.tsx +288 -0
- package/frontend/src/pages/AutomationsPage.tsx +354 -0
- package/frontend/src/pages/ChatPage.tsx +831 -0
- package/frontend/src/pages/DeploymentsPage.tsx +275 -0
- package/frontend/src/pages/FilesPage.tsx +384 -0
- package/frontend/src/pages/HealthPage.tsx +91 -0
- package/frontend/src/pages/IntegrationsPage.tsx +382 -0
- package/frontend/src/pages/LoginPage.tsx +237 -0
- package/frontend/src/pages/MemoryPage.tsx +1192 -0
- package/frontend/src/pages/SandboxPage.tsx +113 -0
- package/frontend/src/pages/SettingsPage.tsx +662 -0
- package/frontend/src/pages/SkillsPage.tsx +618 -0
- package/frontend/src/pages/TasksPage.tsx +392 -0
- package/frontend/src/pages/UsagePage.tsx +260 -0
- package/frontend/src/types.ts +163 -0
- package/frontend/tsconfig.app.json +25 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/tsconfig.node.json +24 -0
- package/frontend/vite.config.ts +24 -0
- package/package.json +8 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
- package/src/cli/backup.mjs +172 -1
- package/src/cli/setup.mjs +24 -0
- package/src/server/rest.mjs +56 -7
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { useState, useEffect, useRef, useMemo } from "react"
|
|
3
|
+
import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, WheelEvent as ReactWheelEvent } from "react"
|
|
4
|
+
import type { MemoryNode, ResearchItem } from "../types"
|
|
5
|
+
import type { ChatThread } from "../api"
|
|
6
|
+
import { renderMarkdown } from "./MarkdownUtils"
|
|
7
|
+
|
|
8
|
+
interface Graph3DProps {
|
|
9
|
+
threads: ChatThread[]
|
|
10
|
+
memoryNodes: MemoryNode[]
|
|
11
|
+
researchItems: ResearchItem[]
|
|
12
|
+
onOpenThread: (threadId: string) => void
|
|
13
|
+
onGroundMemoryNode: (slug: string) => void
|
|
14
|
+
selectedGroundingNodes: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VisualNode {
|
|
18
|
+
id: string
|
|
19
|
+
type: "thread" | "memory" | "research"
|
|
20
|
+
title: string
|
|
21
|
+
subtitle: string
|
|
22
|
+
status?: string
|
|
23
|
+
excerpt?: string
|
|
24
|
+
originalData: ChatThread | MemoryNode | ResearchItem
|
|
25
|
+
// Initial 3D coordinates
|
|
26
|
+
x: number
|
|
27
|
+
y: number
|
|
28
|
+
z: number
|
|
29
|
+
// Projected screen coordinates
|
|
30
|
+
px?: number
|
|
31
|
+
py?: number
|
|
32
|
+
pz?: number
|
|
33
|
+
scale?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface VisualLink {
|
|
37
|
+
source: VisualNode
|
|
38
|
+
target: VisualNode
|
|
39
|
+
opacity: number
|
|
40
|
+
color?: string
|
|
41
|
+
dashPattern?: number[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function Graph3D({
|
|
45
|
+
threads,
|
|
46
|
+
memoryNodes,
|
|
47
|
+
researchItems,
|
|
48
|
+
onOpenThread,
|
|
49
|
+
onGroundMemoryNode,
|
|
50
|
+
selectedGroundingNodes,
|
|
51
|
+
}: Graph3DProps) {
|
|
52
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
53
|
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
54
|
+
|
|
55
|
+
// Projection configuration state
|
|
56
|
+
const [zoom, setZoom] = useState<number>(1.2)
|
|
57
|
+
const [selectedNode, setSelectedNode] = useState<VisualNode | null>(null)
|
|
58
|
+
const [hoveredNode, setHoveredNode] = useState<VisualNode | null>(null)
|
|
59
|
+
const [dragging, setDragging] = useState<boolean>(false)
|
|
60
|
+
const [visibleTypes, setVisibleTypes] = useState({
|
|
61
|
+
research: true,
|
|
62
|
+
observations: true,
|
|
63
|
+
rules: false,
|
|
64
|
+
threads: false,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Dragging and orbital rotation state (stored in refs for high frame rate access)
|
|
68
|
+
const angleX = useRef<number>(0.3)
|
|
69
|
+
const angleY = useRef<number>(0.5)
|
|
70
|
+
const isDragging = useRef<boolean>(false)
|
|
71
|
+
const lastMousePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
|
72
|
+
const velocityX = useRef<number>(0.002) // Auto rotation speed
|
|
73
|
+
const velocityY = useRef<number>(0)
|
|
74
|
+
|
|
75
|
+
// Generate deterministic 3D nodes using Fibonacci Sphere layout
|
|
76
|
+
const nodes = useMemo(() => {
|
|
77
|
+
const list: VisualNode[] = []
|
|
78
|
+
|
|
79
|
+
// 1. Thread nodes
|
|
80
|
+
if (visibleTypes.threads) {
|
|
81
|
+
threads.forEach((t) => {
|
|
82
|
+
list.push({
|
|
83
|
+
id: `thread-${t.id}`,
|
|
84
|
+
type: 'thread',
|
|
85
|
+
title: t.title || 'Untitled Thread',
|
|
86
|
+
subtitle: `${t.turns} ${t.turns === 1 ? 'turn' : 'turns'}`,
|
|
87
|
+
excerpt: `Conversation thread last updated ${new Date(t.lastUpdated).toLocaleDateString()}`,
|
|
88
|
+
originalData: t,
|
|
89
|
+
x: 0,
|
|
90
|
+
y: 0,
|
|
91
|
+
z: 0,
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Memory nodes
|
|
97
|
+
memoryNodes.forEach((m) => {
|
|
98
|
+
const category = (m.category || 'general').toLowerCase()
|
|
99
|
+
const isRule = ['invariants', 'preferences', 'patterns', 'anti-patterns'].includes(category)
|
|
100
|
+
if (isRule) {
|
|
101
|
+
if (!visibleTypes.rules) return
|
|
102
|
+
} else {
|
|
103
|
+
if (!visibleTypes.observations) return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
list.push({
|
|
107
|
+
id: `memory-${m.slug}`,
|
|
108
|
+
type: 'memory',
|
|
109
|
+
title: m.title || m.slug,
|
|
110
|
+
subtitle: `Memory Node • ${m.category || 'general'}`,
|
|
111
|
+
status: m.status || 'active',
|
|
112
|
+
excerpt: m.excerpt || m.body || 'No summary details available.',
|
|
113
|
+
originalData: m,
|
|
114
|
+
x: 0,
|
|
115
|
+
y: 0,
|
|
116
|
+
z: 0,
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// 3. Research nodes
|
|
121
|
+
if (visibleTypes.research) {
|
|
122
|
+
researchItems.forEach((r) => {
|
|
123
|
+
list.push({
|
|
124
|
+
id: `research-${r.id}`,
|
|
125
|
+
type: 'research',
|
|
126
|
+
title: r.topic,
|
|
127
|
+
subtitle: `Research Topic • ${r.priority} priority`,
|
|
128
|
+
status: r.status,
|
|
129
|
+
excerpt: r.notes || `Research task currently ${r.status}. Created on ${new Date(r.created_at).toLocaleDateString()}`,
|
|
130
|
+
originalData: r,
|
|
131
|
+
x: 0,
|
|
132
|
+
y: 0,
|
|
133
|
+
z: 0,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Apply Fibonacci Sphere algorithm for beautiful uniform distribution
|
|
139
|
+
const N = list.length
|
|
140
|
+
const radius = 220 // Size of the sphere volume
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < N; i++) {
|
|
143
|
+
// Golden ratio spacing
|
|
144
|
+
const phi = Math.acos(1 - (2 * (i + 0.5)) / N)
|
|
145
|
+
const theta = Math.PI * (1 + Math.sqrt(5)) * (i + 0.5)
|
|
146
|
+
|
|
147
|
+
list[i].x = radius * Math.sin(phi) * Math.cos(theta)
|
|
148
|
+
list[i].y = radius * Math.sin(phi) * Math.sin(theta)
|
|
149
|
+
list[i].z = radius * Math.cos(phi)
|
|
150
|
+
|
|
151
|
+
// Add a tiny bit of cluster jitter based on node type to separate planes slightly
|
|
152
|
+
if (list[i].type === 'thread') {
|
|
153
|
+
list[i].y += 30
|
|
154
|
+
} else if (list[i].type === 'research') {
|
|
155
|
+
list[i].x -= 30
|
|
156
|
+
} else {
|
|
157
|
+
list[i].z += 20
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return list
|
|
162
|
+
}, [threads, memoryNodes, researchItems, visibleTypes])
|
|
163
|
+
|
|
164
|
+
// Establish links/connections between related items
|
|
165
|
+
const links = useMemo(() => {
|
|
166
|
+
const list: VisualLink[] = []
|
|
167
|
+
|
|
168
|
+
// Match criteria for a beautiful connected web
|
|
169
|
+
nodes.forEach((nodeA, idx) => {
|
|
170
|
+
// 1. Link Research nodes to their matching Memory node
|
|
171
|
+
if (nodeA.type === "research") {
|
|
172
|
+
const researchItem = nodeA.originalData as ResearchItem
|
|
173
|
+
if (researchItem.node_slug) {
|
|
174
|
+
const targetNode = nodes.find(n => n.type === "memory" && (n.originalData as any).slug === researchItem.node_slug)
|
|
175
|
+
if (targetNode) {
|
|
176
|
+
list.push({ source: nodeA, target: targetNode, opacity: 0.8 })
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 2. Link Memory nodes based on explicit SSSS v2 relations (related, supersedes, contradicts)
|
|
182
|
+
if (nodeA.type === "memory") {
|
|
183
|
+
const memA = nodeA.originalData as MemoryNode
|
|
184
|
+
|
|
185
|
+
// Explicit related links (purple)
|
|
186
|
+
if (Array.isArray(memA.related)) {
|
|
187
|
+
memA.related.forEach(relSlug => {
|
|
188
|
+
const targetNode = nodes.find(n => n.type === "memory" && (n.originalData as any).slug === relSlug)
|
|
189
|
+
if (targetNode) {
|
|
190
|
+
list.push({
|
|
191
|
+
source: nodeA,
|
|
192
|
+
target: targetNode,
|
|
193
|
+
opacity: 0.7,
|
|
194
|
+
color: "rgba(167, 139, 250, 0.55)"
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Explicit contradicts links (red)
|
|
201
|
+
if (Array.isArray(memA.contradicts)) {
|
|
202
|
+
memA.contradicts.forEach(conSlug => {
|
|
203
|
+
const targetNode = nodes.find(n => n.type === "memory" && (n.originalData as any).slug === conSlug)
|
|
204
|
+
if (targetNode) {
|
|
205
|
+
list.push({
|
|
206
|
+
source: nodeA,
|
|
207
|
+
target: targetNode,
|
|
208
|
+
opacity: 0.9,
|
|
209
|
+
color: "rgba(239, 68, 68, 0.7)"
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Explicit supersedes links (dashed orange)
|
|
216
|
+
if (Array.isArray(memA.supersedes)) {
|
|
217
|
+
memA.supersedes.forEach(supSlug => {
|
|
218
|
+
const targetNode = nodes.find(n => n.type === "memory" && (n.originalData as any).slug === supSlug)
|
|
219
|
+
if (targetNode) {
|
|
220
|
+
list.push({
|
|
221
|
+
source: nodeA,
|
|
222
|
+
target: targetNode,
|
|
223
|
+
opacity: 0.8,
|
|
224
|
+
color: "rgba(245, 158, 11, 0.6)",
|
|
225
|
+
dashPattern: [3, 3]
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback: Link Memory nodes of similar categories or tags if no explicit relations exist to maintain beautiful web structure
|
|
232
|
+
if (!memA.related?.length && !memA.contradicts?.length && !memA.supersedes?.length) {
|
|
233
|
+
let count = 0
|
|
234
|
+
for (let j = idx + 1; j < nodes.length; j++) {
|
|
235
|
+
const nodeB = nodes[j]
|
|
236
|
+
if (nodeB.type === "memory" && count < 2) {
|
|
237
|
+
const memB = nodeB.originalData as MemoryNode
|
|
238
|
+
if (memA.category && memA.category === memB.category) {
|
|
239
|
+
list.push({ source: nodeA, target: nodeB, opacity: 0.4 })
|
|
240
|
+
count++
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 3. Link Chat threads to grounded memory nodes if titles/slugs align or randomly to create constellations
|
|
248
|
+
if (nodeA.type === "thread") {
|
|
249
|
+
// Find 1-2 memory nodes to anchor the thread node
|
|
250
|
+
let matches = 0
|
|
251
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
252
|
+
const nodeB = nodes[j]
|
|
253
|
+
if (nodeB.type === "memory" && matches < 1) {
|
|
254
|
+
// Draw a thin connection
|
|
255
|
+
list.push({ source: nodeA, target: nodeB, opacity: 0.35 })
|
|
256
|
+
matches++
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return list
|
|
263
|
+
}, [nodes])
|
|
264
|
+
|
|
265
|
+
// Render loop
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
const canvas = canvasRef.current
|
|
268
|
+
if (!canvas) return
|
|
269
|
+
|
|
270
|
+
const ctx = canvas.getContext('2d')
|
|
271
|
+
if (!ctx) return
|
|
272
|
+
|
|
273
|
+
let animationFrameId: number
|
|
274
|
+
const focalLength = 400
|
|
275
|
+
|
|
276
|
+
// Set dimensions based on client bounds
|
|
277
|
+
const resizeCanvas = () => {
|
|
278
|
+
const rect = containerRef.current?.getBoundingClientRect()
|
|
279
|
+
canvas.width = (rect?.width || window.innerWidth) * window.devicePixelRatio
|
|
280
|
+
canvas.height = (rect?.height || window.innerHeight) * window.devicePixelRatio
|
|
281
|
+
canvas.style.width = '100%'
|
|
282
|
+
canvas.style.height = '100%'
|
|
283
|
+
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
|
|
284
|
+
}
|
|
285
|
+
resizeCanvas()
|
|
286
|
+
window.addEventListener('resize', resizeCanvas)
|
|
287
|
+
|
|
288
|
+
const draw = () => {
|
|
289
|
+
const width = canvas.width / window.devicePixelRatio
|
|
290
|
+
const height = canvas.height / window.devicePixelRatio
|
|
291
|
+
ctx.clearRect(0, 0, width, height)
|
|
292
|
+
|
|
293
|
+
const centerX = width / 2
|
|
294
|
+
const centerY = height / 2
|
|
295
|
+
|
|
296
|
+
// Apply orbital inertia
|
|
297
|
+
if (!isDragging.current) {
|
|
298
|
+
angleY.current += velocityX.current
|
|
299
|
+
angleX.current += velocityY.current
|
|
300
|
+
// Smooth deceleration
|
|
301
|
+
velocityX.current *= 0.98
|
|
302
|
+
velocityY.current *= 0.98
|
|
303
|
+
|
|
304
|
+
// Maintain gentle orbital motion if rotation dies down completely
|
|
305
|
+
if (Math.abs(velocityX.current) < 0.001) {
|
|
306
|
+
velocityX.current = 0.0015
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const cosX = Math.cos(angleX.current)
|
|
311
|
+
const sinX = Math.sin(angleX.current)
|
|
312
|
+
const cosY = Math.cos(angleY.current)
|
|
313
|
+
const sinY = Math.sin(angleY.current)
|
|
314
|
+
|
|
315
|
+
// Project all nodes first
|
|
316
|
+
nodes.forEach((node) => {
|
|
317
|
+
// Rotate Y axis
|
|
318
|
+
const x1 = node.x * cosY + node.z * sinY
|
|
319
|
+
const z1 = -node.x * sinY + node.z * cosY
|
|
320
|
+
|
|
321
|
+
// Rotate X axis
|
|
322
|
+
const y2 = node.y * cosX - z1 * sinX
|
|
323
|
+
const z2 = node.y * sinX + z1 * cosX
|
|
324
|
+
|
|
325
|
+
// Perspective Projection
|
|
326
|
+
const scale = focalLength / (focalLength + z2)
|
|
327
|
+
node.px = centerX + x1 * scale * zoom
|
|
328
|
+
node.py = centerY + y2 * scale * zoom
|
|
329
|
+
node.pz = z2
|
|
330
|
+
node.scale = scale
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Sort nodes by depth (z-index drawing) to render back elements first
|
|
334
|
+
const sortedNodes = [...nodes].sort((a, b) => (b.pz || 0) - (a.pz || 0))
|
|
335
|
+
|
|
336
|
+
// ─── DRAW LINKS ───
|
|
337
|
+
ctx.lineWidth = 1.0
|
|
338
|
+
links.forEach((link) => {
|
|
339
|
+
const { source, target, opacity, color, dashPattern } = link
|
|
340
|
+
if (source.px === undefined || source.py === undefined || target.px === undefined || target.py === undefined) return
|
|
341
|
+
|
|
342
|
+
// Fade links that are deeper or far
|
|
343
|
+
const avgZ = ((source.pz || 0) + (target.pz || 0)) / 2
|
|
344
|
+
const depthFade = Math.max(0.1, Math.min(1.0, 1 - (avgZ + 200) / 500))
|
|
345
|
+
|
|
346
|
+
// Highlight links connected to the hovered or selected nodes
|
|
347
|
+
const isHighlight =
|
|
348
|
+
hoveredNode?.id === source.id ||
|
|
349
|
+
hoveredNode?.id === target.id ||
|
|
350
|
+
selectedNode?.id === source.id ||
|
|
351
|
+
selectedNode?.id === target.id
|
|
352
|
+
|
|
353
|
+
// Glow connections
|
|
354
|
+
ctx.beginPath()
|
|
355
|
+
ctx.moveTo(source.px, source.py)
|
|
356
|
+
ctx.lineTo(target.px, target.py)
|
|
357
|
+
|
|
358
|
+
if (dashPattern) {
|
|
359
|
+
ctx.setLineDash(dashPattern)
|
|
360
|
+
} else {
|
|
361
|
+
ctx.setLineDash([])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (isHighlight) {
|
|
365
|
+
ctx.strokeStyle = color || (source.type === "memory" ? "rgba(139, 92, 246, 0.65)" : source.type === "thread" ? "rgba(6, 182, 212, 0.65)" : "rgba(245, 158, 11, 0.65)")
|
|
366
|
+
ctx.lineWidth = 1.8
|
|
367
|
+
} else {
|
|
368
|
+
ctx.strokeStyle = color
|
|
369
|
+
? color.replace(/[\d.]+\)$/, `${opacity * 0.4 * depthFade})`)
|
|
370
|
+
: `rgba(100, 116, 139, ${opacity * 0.25 * depthFade})`
|
|
371
|
+
ctx.lineWidth = 0.8
|
|
372
|
+
}
|
|
373
|
+
ctx.stroke()
|
|
374
|
+
ctx.setLineDash([]) // Reset dash pattern
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// ─── DRAW NODES ───
|
|
378
|
+
sortedNodes.forEach((node) => {
|
|
379
|
+
if (node.px === undefined || node.py === undefined || node.scale === undefined) return
|
|
380
|
+
|
|
381
|
+
// Support confidence decay mapping to node size
|
|
382
|
+
const confidenceMultiplier = node.type === "memory" && (node.originalData as any).confidence !== undefined
|
|
383
|
+
? Math.max(0.4, (node.originalData as any).confidence)
|
|
384
|
+
: 1.0
|
|
385
|
+
|
|
386
|
+
const size = Math.max(4, Math.min(22, 10 * node.scale * zoom * confidenceMultiplier))
|
|
387
|
+
const isHovered = hoveredNode?.id === node.id
|
|
388
|
+
const isSelected = selectedNode?.id === node.id
|
|
389
|
+
const isGrounded = node.type === "memory" && selectedGroundingNodes.includes((node.originalData as any).slug)
|
|
390
|
+
|
|
391
|
+
// Draw radial glow
|
|
392
|
+
const gradient = ctx.createRadialGradient(
|
|
393
|
+
node.px,
|
|
394
|
+
node.py,
|
|
395
|
+
size * 0.1,
|
|
396
|
+
node.px,
|
|
397
|
+
node.py,
|
|
398
|
+
size * (isHovered || isSelected ? 2.5 : 1.5)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
let color = "#8b5cf6" // memory observations (purple)
|
|
402
|
+
let glowColor = "rgba(139, 92, 246, 0.2)"
|
|
403
|
+
|
|
404
|
+
if (node.type === "memory") {
|
|
405
|
+
const category = ((node.originalData as MemoryNode).category || 'general').toLowerCase()
|
|
406
|
+
const isRule = ['invariants', 'preferences', 'patterns', 'anti-patterns'].includes(category)
|
|
407
|
+
if (isRule) {
|
|
408
|
+
color = "#6366f1" // developer rules (indigo)
|
|
409
|
+
glowColor = "rgba(99, 102, 241, 0.2)"
|
|
410
|
+
}
|
|
411
|
+
} else if (node.type === "thread") {
|
|
412
|
+
color = "#06b6d4" // thread nodes (cyan)
|
|
413
|
+
glowColor = "rgba(6, 182, 212, 0.2)"
|
|
414
|
+
} else if (node.type === "research") {
|
|
415
|
+
color = "#f59e0b" // research nodes (gold)
|
|
416
|
+
glowColor = "rgba(245, 158, 11, 0.2)"
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
gradient.addColorStop(0, color)
|
|
420
|
+
gradient.addColorStop(0.4, color)
|
|
421
|
+
gradient.addColorStop(1, glowColor)
|
|
422
|
+
|
|
423
|
+
// Faint shadow/depth sorting opacity and confidence scaling
|
|
424
|
+
const nodeDepthOpacity = Math.max(0.2, Math.min(1.0, 1 - (node.pz || 0 + 200) / 450))
|
|
425
|
+
const confidenceOpacity = node.type === "memory" && (node.originalData as any).confidence !== undefined
|
|
426
|
+
? Math.max(0.35, (node.originalData as any).confidence)
|
|
427
|
+
: 1.0
|
|
428
|
+
|
|
429
|
+
ctx.globalAlpha = nodeDepthOpacity * confidenceOpacity
|
|
430
|
+
|
|
431
|
+
ctx.beginPath()
|
|
432
|
+
ctx.arc(node.px, node.py, size * (isHovered || isSelected ? 2.5 : 1.5), 0, Math.PI * 2)
|
|
433
|
+
ctx.fillStyle = gradient
|
|
434
|
+
ctx.fill()
|
|
435
|
+
|
|
436
|
+
// Core solid sphere
|
|
437
|
+
ctx.beginPath()
|
|
438
|
+
ctx.arc(node.px, node.py, size * 0.7, 0, Math.PI * 2)
|
|
439
|
+
ctx.fillStyle = isHovered || isSelected ? '#ffffff' : color
|
|
440
|
+
ctx.fill()
|
|
441
|
+
|
|
442
|
+
// Highlight ring around selected node
|
|
443
|
+
if (isSelected || isGrounded) {
|
|
444
|
+
ctx.beginPath()
|
|
445
|
+
ctx.arc(node.px, node.py, size * 1.5, 0, Math.PI * 2)
|
|
446
|
+
ctx.strokeStyle = '#ffffff'
|
|
447
|
+
ctx.lineWidth = 1.5
|
|
448
|
+
ctx.stroke()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
ctx.globalAlpha = 1.0 // Reset opacity
|
|
452
|
+
|
|
453
|
+
// ─── RENDER LABELS ───
|
|
454
|
+
// Display labels for hovered/selected nodes or very close front nodes
|
|
455
|
+
if (isHovered || isSelected || (node.scale > 1.15 && node.pz && node.pz < -100)) {
|
|
456
|
+
ctx.font = isHovered || isSelected ? 'bold 12px var(--font-sans, system-ui)' : '10px var(--font-sans, system-ui)'
|
|
457
|
+
ctx.fillStyle = isHovered || isSelected ? '#ffffff' : 'rgba(255, 255, 255, 0.75)'
|
|
458
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.9)'
|
|
459
|
+
ctx.shadowBlur = 4
|
|
460
|
+
|
|
461
|
+
const labelText = node.title.length > 28 ? node.title.substring(0, 26) + '...' : node.title
|
|
462
|
+
ctx.fillText(labelText, node.px + size * 1.6, node.py + size * 0.4)
|
|
463
|
+
|
|
464
|
+
// Subtitle for hovered/selected nodes
|
|
465
|
+
if (isHovered || isSelected) {
|
|
466
|
+
ctx.font = '9px var(--font-sans, system-ui)'
|
|
467
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.55)'
|
|
468
|
+
ctx.fillText(node.subtitle, node.px + size * 1.6, node.py + size * 0.4 + 14)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Reset shadows
|
|
472
|
+
ctx.shadowBlur = 0
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
animationFrameId = requestAnimationFrame(draw)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
draw()
|
|
480
|
+
|
|
481
|
+
return () => {
|
|
482
|
+
cancelAnimationFrame(animationFrameId)
|
|
483
|
+
window.removeEventListener('resize', resizeCanvas)
|
|
484
|
+
}
|
|
485
|
+
}, [nodes, links, zoom, hoveredNode, selectedNode, selectedGroundingNodes])
|
|
486
|
+
|
|
487
|
+
// Mouse / Touch Interaction Handlers
|
|
488
|
+
const handleStartDrag = (clientX: number, clientY: number) => {
|
|
489
|
+
isDragging.current = true
|
|
490
|
+
setDragging(true)
|
|
491
|
+
lastMousePos.current = { x: clientX, y: clientY }
|
|
492
|
+
velocityX.current = 0
|
|
493
|
+
velocityY.current = 0
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const handleDrag = (clientX: number, clientY: number) => {
|
|
497
|
+
if (!isDragging.current) return
|
|
498
|
+
const dx = clientX - lastMousePos.current.x
|
|
499
|
+
const dy = clientY - lastMousePos.current.y
|
|
500
|
+
|
|
501
|
+
// Scale rotations based on coordinate displacement
|
|
502
|
+
angleY.current += dx * 0.006
|
|
503
|
+
angleX.current += dy * 0.006
|
|
504
|
+
|
|
505
|
+
// Keep velocities for inertia on release
|
|
506
|
+
velocityX.current = dx * 0.005
|
|
507
|
+
velocityY.current = dy * 0.005
|
|
508
|
+
|
|
509
|
+
lastMousePos.current = { x: clientX, y: clientY }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const handleEndDrag = () => {
|
|
513
|
+
isDragging.current = false
|
|
514
|
+
setDragging(false)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Handle canvas mouse move to detect hovering
|
|
518
|
+
const handleMouseMove = (e: ReactMouseEvent<HTMLCanvasElement>) => {
|
|
519
|
+
const canvas = canvasRef.current
|
|
520
|
+
if (!canvas) return
|
|
521
|
+
|
|
522
|
+
const rect = canvas.getBoundingClientRect()
|
|
523
|
+
const mouseX = e.clientX - rect.left
|
|
524
|
+
const mouseY = e.clientY - rect.top
|
|
525
|
+
|
|
526
|
+
if (isDragging.current) {
|
|
527
|
+
handleDrag(e.clientX, e.clientY)
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Detect hovered node
|
|
532
|
+
let closestNode: VisualNode | null = null
|
|
533
|
+
let minDistance = 22 // Capture radius
|
|
534
|
+
|
|
535
|
+
nodes.forEach((node) => {
|
|
536
|
+
if (node.px === undefined || node.py === undefined) return
|
|
537
|
+
const distance = Math.hypot(node.px - mouseX, node.py - mouseY)
|
|
538
|
+
if (distance < minDistance) {
|
|
539
|
+
minDistance = distance
|
|
540
|
+
closestNode = node
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
setHoveredNode(closestNode)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Handle clicks to select nodes
|
|
548
|
+
const handleCanvasClick = () => {
|
|
549
|
+
if (isDragging.current && (Math.abs(velocityX.current) > 0.02 || Math.abs(velocityY.current) > 0.02)) {
|
|
550
|
+
// It was a swift rotation drag, not a clean select click
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
setSelectedNode(hoveredNode)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Zoom Handler
|
|
557
|
+
const handleWheel = (e: ReactWheelEvent<HTMLCanvasElement>) => {
|
|
558
|
+
e.preventDefault()
|
|
559
|
+
const zoomDelta = e.deltaY * -0.001
|
|
560
|
+
setZoom((prev) => Math.max(0.5, Math.min(3.0, prev + zoomDelta)))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Touch handlers
|
|
564
|
+
const handleTouchStart = (e: ReactTouchEvent<HTMLCanvasElement>) => {
|
|
565
|
+
if (e.touches.length === 1) {
|
|
566
|
+
const touch = e.touches[0]
|
|
567
|
+
handleStartDrag(touch.clientX, touch.clientY)
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const handleTouchMove = (e: ReactTouchEvent<HTMLCanvasElement>) => {
|
|
572
|
+
if (e.touches.length === 1) {
|
|
573
|
+
const touch = e.touches[0]
|
|
574
|
+
if (isDragging.current) {
|
|
575
|
+
handleDrag(touch.clientX, touch.clientY)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Update hover for touch interaction
|
|
579
|
+
const canvas = canvasRef.current
|
|
580
|
+
if (!canvas) return
|
|
581
|
+
const rect = canvas.getBoundingClientRect()
|
|
582
|
+
const touchX = touch.clientX - rect.left
|
|
583
|
+
const touchY = touch.clientY - rect.top
|
|
584
|
+
|
|
585
|
+
let closestNode: VisualNode | null = null
|
|
586
|
+
let minDistance = 30 // Larger tap radius
|
|
587
|
+
|
|
588
|
+
nodes.forEach((node) => {
|
|
589
|
+
if (node.px === undefined || node.py === undefined) return
|
|
590
|
+
const distance = Math.hypot(node.px - touchX, node.py - touchY)
|
|
591
|
+
if (distance < minDistance) {
|
|
592
|
+
minDistance = distance
|
|
593
|
+
closestNode = node
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
setHoveredNode(closestNode)
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<div className="graph-3d-wrapper" ref={containerRef} style={{ position: 'relative', width: '100%', height: '100%', background: 'radial-gradient(circle at center, #0b0f19 0%, #030712 100%)', overflow: 'hidden' }}>
|
|
602
|
+
|
|
603
|
+
{/* 3D Projection Canvas */}
|
|
604
|
+
<canvas
|
|
605
|
+
ref={canvasRef}
|
|
606
|
+
onMouseDown={(e) => handleStartDrag(e.clientX, e.clientY)}
|
|
607
|
+
onMouseMove={handleMouseMove}
|
|
608
|
+
onMouseUp={handleEndDrag}
|
|
609
|
+
onMouseLeave={handleEndDrag}
|
|
610
|
+
onClick={handleCanvasClick}
|
|
611
|
+
onWheel={handleWheel}
|
|
612
|
+
onTouchStart={handleTouchStart}
|
|
613
|
+
onTouchMove={handleTouchMove}
|
|
614
|
+
onTouchEnd={handleEndDrag}
|
|
615
|
+
style={{ cursor: dragging ? "grabbing" : hoveredNode ? "pointer" : "grab", display: "block" }}
|
|
616
|
+
/>
|
|
617
|
+
|
|
618
|
+
{/* Cosmic Floating Background Dust overlay */}
|
|
619
|
+
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, pointerEvents: 'none', background: 'radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.05) 0%, transparent 60%)' }} />
|
|
620
|
+
|
|
621
|
+
{/* Floating UI Elements */}
|
|
622
|
+
<div style={{ position: 'absolute', top: 20, left: 20, display: 'flex', flexDirection: 'column', gap: 6, pointerEvents: 'none' }}>
|
|
623
|
+
<h3 style={{ margin: 0, fontSize: 18, color: '#fff', fontWeight: 700, letterSpacing: '0.5px' }}>Sovereign Mind Matrix</h3>
|
|
624
|
+
<p style={{ margin: 0, fontSize: 11, color: 'rgba(255,255,255,0.45)' }}>3D Constellation of Research, Memory & Conversations</p>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Controls Overlay */}
|
|
628
|
+
<div style={{ position: 'absolute', bottom: 20, left: 20, display: 'flex', gap: 8 }}>
|
|
629
|
+
<button
|
|
630
|
+
onClick={() => setZoom(prev => Math.min(3.0, prev + 0.2))}
|
|
631
|
+
style={{ width: 32, height: 32, background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', fontSize: 16, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
632
|
+
title="Zoom In"
|
|
633
|
+
>
|
|
634
|
+
+
|
|
635
|
+
</button>
|
|
636
|
+
<button
|
|
637
|
+
onClick={() => setZoom(prev => Math.max(0.5, prev - 0.2))}
|
|
638
|
+
style={{ width: 32, height: 32, background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', fontSize: 16, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
639
|
+
title="Zoom Out"
|
|
640
|
+
>
|
|
641
|
+
-
|
|
642
|
+
</button>
|
|
643
|
+
<button
|
|
644
|
+
onClick={() => { angleX.current = 0.3; angleY.current = 0.5; setZoom(1.2) }}
|
|
645
|
+
style={{ width: 32, height: 32, background: 'rgba(15,23,42,0.6)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', fontSize: 11, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
646
|
+
title="Reset Camera"
|
|
647
|
+
>
|
|
648
|
+
⟲
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
651
|
+
|
|
652
|
+
{/* Floating Filter Panel (Top Right) */}
|
|
653
|
+
<div
|
|
654
|
+
className="glass"
|
|
655
|
+
style={{
|
|
656
|
+
position: 'absolute',
|
|
657
|
+
top: 20,
|
|
658
|
+
right: 20,
|
|
659
|
+
display: 'flex',
|
|
660
|
+
flexDirection: 'column',
|
|
661
|
+
gap: 10,
|
|
662
|
+
background: 'rgba(10, 15, 30, 0.75)',
|
|
663
|
+
backdropFilter: 'blur(20px)',
|
|
664
|
+
WebkitBackdropFilter: 'blur(20px)',
|
|
665
|
+
padding: '12px 16px',
|
|
666
|
+
borderRadius: 12,
|
|
667
|
+
border: '1px solid rgba(255, 255, 255, 0.08)',
|
|
668
|
+
boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
|
|
669
|
+
width: 220,
|
|
670
|
+
zIndex: 90,
|
|
671
|
+
}}
|
|
672
|
+
>
|
|
673
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
674
|
+
<span style={{ fontSize: 9, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 1 }}>
|
|
675
|
+
Constellation Filters
|
|
676
|
+
</span>
|
|
677
|
+
<span style={{ fontSize: 9, background: 'rgba(255,255,255,0.08)', padding: '1px 5px', borderRadius: 4, color: 'var(--text-secondary)' }}>
|
|
678
|
+
{nodes.length} visible
|
|
679
|
+
</span>
|
|
680
|
+
</div>
|
|
681
|
+
|
|
682
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
683
|
+
{/* 1. Research Projects */}
|
|
684
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--text-secondary)', cursor: 'pointer', userSelect: 'none' }}>
|
|
685
|
+
<input
|
|
686
|
+
type="checkbox"
|
|
687
|
+
checked={visibleTypes.research}
|
|
688
|
+
onChange={() => setVisibleTypes(prev => ({ ...prev, research: !prev.research }))}
|
|
689
|
+
style={{ accentColor: '#f59e0b', cursor: 'pointer' }}
|
|
690
|
+
/>
|
|
691
|
+
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#f59e0b' }} />
|
|
692
|
+
<span style={{ flex: 1 }}>Research Projects</span>
|
|
693
|
+
<span style={{ fontSize: 9, color: 'var(--text-tertiary)' }}>({researchItems.length})</span>
|
|
694
|
+
</label>
|
|
695
|
+
|
|
696
|
+
{/* 2. Observations & Facts */}
|
|
697
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--text-secondary)', cursor: 'pointer', userSelect: 'none' }}>
|
|
698
|
+
<input
|
|
699
|
+
type="checkbox"
|
|
700
|
+
checked={visibleTypes.observations}
|
|
701
|
+
onChange={() => setVisibleTypes(prev => ({ ...prev, observations: !prev.observations }))}
|
|
702
|
+
style={{ accentColor: '#8b5cf6', cursor: 'pointer' }}
|
|
703
|
+
/>
|
|
704
|
+
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#8b5cf6' }} />
|
|
705
|
+
<span style={{ flex: 1 }}>Observations</span>
|
|
706
|
+
<span style={{ fontSize: 9, color: 'var(--text-tertiary)' }}>
|
|
707
|
+
({memoryNodes.filter(m => !['invariants', 'preferences', 'patterns', 'anti-patterns'].includes((m.category || 'general').toLowerCase())).length})
|
|
708
|
+
</span>
|
|
709
|
+
</label>
|
|
710
|
+
|
|
711
|
+
{/* 3. Developer Rules */}
|
|
712
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--text-secondary)', cursor: 'pointer', userSelect: 'none' }}>
|
|
713
|
+
<input
|
|
714
|
+
type="checkbox"
|
|
715
|
+
checked={visibleTypes.rules}
|
|
716
|
+
onChange={() => setVisibleTypes(prev => ({ ...prev, rules: !prev.rules }))}
|
|
717
|
+
style={{ accentColor: '#6366f1', cursor: 'pointer' }}
|
|
718
|
+
/>
|
|
719
|
+
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#6366f1' }} />
|
|
720
|
+
<span style={{ flex: 1 }}>Developer Rules</span>
|
|
721
|
+
<span style={{ fontSize: 9, color: 'var(--text-tertiary)' }}>
|
|
722
|
+
({memoryNodes.filter(m => ['invariants', 'preferences', 'patterns', 'anti-patterns'].includes((m.category || 'general').toLowerCase())).length})
|
|
723
|
+
</span>
|
|
724
|
+
</label>
|
|
725
|
+
|
|
726
|
+
{/* 4. Chat Threads */}
|
|
727
|
+
<label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, color: 'var(--text-secondary)', cursor: 'pointer', userSelect: 'none' }}>
|
|
728
|
+
<input
|
|
729
|
+
type="checkbox"
|
|
730
|
+
checked={visibleTypes.threads}
|
|
731
|
+
onChange={() => setVisibleTypes(prev => ({ ...prev, threads: !prev.threads }))}
|
|
732
|
+
style={{ accentColor: '#06b6d4', cursor: 'pointer' }}
|
|
733
|
+
/>
|
|
734
|
+
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#06b6d4' }} />
|
|
735
|
+
<span style={{ flex: 1 }}>Conversations</span>
|
|
736
|
+
<span style={{ fontSize: 9, color: 'var(--text-tertiary)' }}>({threads.length})</span>
|
|
737
|
+
</label>
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
{/* Selected Node Glass detail overlay drawer */}
|
|
742
|
+
{selectedNode && (
|
|
743
|
+
<div className="node-detail-drawer animate-slide-in">
|
|
744
|
+
<div className="drawer-header">
|
|
745
|
+
<span className={`drawer-badge badge-${selectedNode.type}`}>
|
|
746
|
+
{selectedNode.type === "memory" ? "🧠 Knowledge" : selectedNode.type === "thread" ? "💬 Thread" : "🕵️ Research"}
|
|
747
|
+
</span>
|
|
748
|
+
<button className="drawer-close-btn" onClick={() => setSelectedNode(null)}>×</button>
|
|
749
|
+
</div>
|
|
750
|
+
<div className="drawer-body">
|
|
751
|
+
<h4 className="drawer-title">{selectedNode.title}</h4>
|
|
752
|
+
<span className="drawer-subtitle">{selectedNode.subtitle}</span>
|
|
753
|
+
|
|
754
|
+
{selectedNode.type === "memory" && (
|
|
755
|
+
<div className="drawer-meta-grid" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "8px", margin: "12px 0", fontSize: "11px", background: "rgba(255,255,255,0.03)", padding: "8px", borderRadius: "6px", border: "1px solid rgba(255,255,255,0.05)" }}>
|
|
756
|
+
{(selectedNode.originalData as any).confidence !== undefined && (
|
|
757
|
+
<div>
|
|
758
|
+
<span style={{ color: "rgba(255,255,255,0.45)" }}>Confidence:</span>{" "}
|
|
759
|
+
<strong style={{ color: (selectedNode.originalData as any).confidence > 0.7 ? "#22c55e" : "#f59e0b" }}>
|
|
760
|
+
{Math.round((selectedNode.originalData as any).confidence * 100)}%
|
|
761
|
+
</strong>
|
|
762
|
+
</div>
|
|
763
|
+
)}
|
|
764
|
+
{(selectedNode.originalData as any).importance !== undefined && (
|
|
765
|
+
<div>
|
|
766
|
+
<span style={{ color: "rgba(255,255,255,0.45)" }}>Importance:</span>{" "}
|
|
767
|
+
<strong style={{ color: "#a78bfa" }}>★ {(selectedNode.originalData as any).importance}/5</strong>
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
{selectedNode.status && (
|
|
771
|
+
<div style={{ gridColumn: "span 2" }}>
|
|
772
|
+
<span style={{ color: "rgba(255,255,255,0.45)" }}>Status:</span>{" "}
|
|
773
|
+
<span className={`value status-${selectedNode.status.toLowerCase().replace(/_/g, "-")}`} style={{ fontSize: "10px", padding: "2px 6px", borderRadius: "10px", background: "rgba(255,255,255,0.08)", textTransform: "uppercase" }}>
|
|
774
|
+
{selectedNode.status}
|
|
775
|
+
</span>
|
|
776
|
+
</div>
|
|
777
|
+
)}
|
|
778
|
+
</div>
|
|
779
|
+
)}
|
|
780
|
+
|
|
781
|
+
{selectedNode.type === "memory" && ((selectedNode.originalData as any).related?.length || (selectedNode.originalData as any).contradicts?.length || (selectedNode.originalData as any).supersedes?.length) && (
|
|
782
|
+
<div className="drawer-relations" style={{ fontSize: "11px", margin: "12px 0" }}>
|
|
783
|
+
<span style={{ color: "rgba(255,255,255,0.45)", display: "block", marginBottom: "4px" }}>Ontology Relations:</span>
|
|
784
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: "4px" }}>
|
|
785
|
+
{(selectedNode.originalData as any).related?.map((slug: string) => (
|
|
786
|
+
<span key={slug} style={{ color: "#a78bfa", background: "rgba(139,92,246,0.1)", padding: "1px 6px", borderRadius: "4px" }}>🔗 {slug}</span>
|
|
787
|
+
))}
|
|
788
|
+
{(selectedNode.originalData as any).contradicts?.map((slug: string) => (
|
|
789
|
+
<span key={slug} style={{ color: "#ef4444", background: "rgba(239,68,68,0.1)", padding: "1px 6px", borderRadius: "4px" }}>⚠️ contradicts: {slug}</span>
|
|
790
|
+
))}
|
|
791
|
+
{(selectedNode.originalData as any).supersedes?.map((slug: string) => (
|
|
792
|
+
<span key={slug} style={{ color: "#f59e0b", background: "rgba(245,158,11,0.1)", padding: "1px 6px", borderRadius: "4px" }}>✓ supersedes: {slug}</span>
|
|
793
|
+
))}
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
)}
|
|
797
|
+
|
|
798
|
+
{selectedNode.type === "memory" ? (
|
|
799
|
+
<div
|
|
800
|
+
className="drawer-markdown-content"
|
|
801
|
+
style={{
|
|
802
|
+
marginTop: 12,
|
|
803
|
+
borderTop: '1px solid rgba(255,255,255,0.06)',
|
|
804
|
+
paddingTop: 12,
|
|
805
|
+
fontSize: '13px',
|
|
806
|
+
color: 'var(--text-secondary)',
|
|
807
|
+
lineHeight: '1.6',
|
|
808
|
+
}}
|
|
809
|
+
>
|
|
810
|
+
{renderMarkdown((selectedNode.originalData as any).content || (selectedNode.originalData as any).body || selectedNode.excerpt || 'No content details available.')}
|
|
811
|
+
</div>
|
|
812
|
+
) : (
|
|
813
|
+
<p className="drawer-excerpt" style={{ whiteSpace: "pre-wrap" }}>{selectedNode.excerpt}</p>
|
|
814
|
+
)}
|
|
815
|
+
</div>
|
|
816
|
+
<div className="drawer-footer">
|
|
817
|
+
{selectedNode.type === "thread" && (
|
|
818
|
+
<button
|
|
819
|
+
className="drawer-action-btn btn-cyan"
|
|
820
|
+
onClick={() => {
|
|
821
|
+
onOpenThread((selectedNode.originalData as any).id)
|
|
822
|
+
setSelectedNode(null)
|
|
823
|
+
}}
|
|
824
|
+
>
|
|
825
|
+
Open Thread
|
|
826
|
+
</button>
|
|
827
|
+
)}
|
|
828
|
+
{selectedNode.type === "memory" && (
|
|
829
|
+
<button
|
|
830
|
+
className="drawer-action-btn btn-purple"
|
|
831
|
+
onClick={() => {
|
|
832
|
+
onGroundMemoryNode((selectedNode.originalData as any).slug)
|
|
833
|
+
}}
|
|
834
|
+
>
|
|
835
|
+
{selectedGroundingNodes.includes((selectedNode.originalData as any).slug)
|
|
836
|
+
? "Remove Grounding Context"
|
|
837
|
+
: "Ground Chat with Node"}
|
|
838
|
+
</button>
|
|
839
|
+
)}
|
|
840
|
+
{selectedNode.type === "research" && (
|
|
841
|
+
<div style={{ width: "100%", fontSize: 11, color: "rgba(255,255,255,0.4)", textAlign: "center", fontStyle: "italic", padding: "8px 0" }}>
|
|
842
|
+
Background intelligence agent running...
|
|
843
|
+
</div>
|
|
844
|
+
)}
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
</div>
|
|
849
|
+
)
|
|
850
|
+
}
|