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,1192 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react"
|
|
2
|
+
import { useSearchParams } from "react-router-dom"
|
|
3
|
+
import { listMemory, searchMemory, readMemory, fetchConflicts, resolveConflict, saveMemory, createMemory, deleteMemory, fetchSessions, deleteSession } from "../api"
|
|
4
|
+
import type { MemoryNode, Conflict } from "../types"
|
|
5
|
+
import type { SessionSummary } from "../api"
|
|
6
|
+
|
|
7
|
+
const CATEGORIES = [
|
|
8
|
+
"all", "invariants", "preferences", "anti-patterns", "patterns",
|
|
9
|
+
"decisions", "concepts", "facts", "lore"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function renderMarkdown(md: string) {
|
|
13
|
+
if (!md) return "";
|
|
14
|
+
let html = md
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">");
|
|
18
|
+
|
|
19
|
+
// Headings
|
|
20
|
+
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
21
|
+
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
22
|
+
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
23
|
+
|
|
24
|
+
// Bold & Italic
|
|
25
|
+
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
26
|
+
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
27
|
+
|
|
28
|
+
// Code blocks
|
|
29
|
+
html = html.replace(/```([\s\S]*?)```/g, "<pre style=\"background:rgba(0,0,0,0.3);padding:12px;border-radius:6px;border:1px solid rgba(255,255,255,0.06);font-family:monospace;overflow-x:auto;\"><code>$1</code></pre>");
|
|
30
|
+
html = html.replace(/`(.*?)`/g, "<code style=\"background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;font-family:monospace;\">$1</code>");
|
|
31
|
+
|
|
32
|
+
// Blockquotes
|
|
33
|
+
html = html.replace(/^> (.*$)/gim, "<blockquote style=\"border-left:4px solid var(--purple-primary);padding-left:14px;color:var(--text-secondary);margin:16px 0;font-style:italic;\">$1</blockquote>");
|
|
34
|
+
|
|
35
|
+
// Lists
|
|
36
|
+
html = html.replace(/^- (.*$)/gim, "<li style=\"margin-left:20px;list-style-type:disc;\">$1</li>");
|
|
37
|
+
|
|
38
|
+
// Linebreaks
|
|
39
|
+
html = html.replace(/\n/g, "<br />");
|
|
40
|
+
|
|
41
|
+
return <div dangerouslySetInnerHTML={{ __html: html }} className="markdown-preview-block" />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function markdownToHtml(md: string): string {
|
|
45
|
+
if (!md) return "";
|
|
46
|
+
let html = md
|
|
47
|
+
.replace(/&/g, "&")
|
|
48
|
+
.replace(/</g, "<")
|
|
49
|
+
.replace(/>/g, ">");
|
|
50
|
+
|
|
51
|
+
// Headings
|
|
52
|
+
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
53
|
+
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
54
|
+
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
55
|
+
|
|
56
|
+
// Bold & Italic
|
|
57
|
+
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
58
|
+
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
59
|
+
|
|
60
|
+
// Blockquotes
|
|
61
|
+
html = html.replace(/^> (.*$)/gim, "<blockquote>$1</blockquote>");
|
|
62
|
+
|
|
63
|
+
// Lists
|
|
64
|
+
html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
|
|
65
|
+
|
|
66
|
+
// Newlines
|
|
67
|
+
html = html.replace(/\n/g, "<br />");
|
|
68
|
+
|
|
69
|
+
return html;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function htmlToMarkdown(html: string): string {
|
|
73
|
+
if (!html) return "";
|
|
74
|
+
let md = html;
|
|
75
|
+
|
|
76
|
+
// Replace <br> and <br /> with newline
|
|
77
|
+
md = md.replace(/<br\s*\/?>/gi, "\n");
|
|
78
|
+
|
|
79
|
+
// Headings
|
|
80
|
+
md = md.replace(/<h1>(.*?)<\/h1>/gi, "# $1\n");
|
|
81
|
+
md = md.replace(/<h2>(.*?)<\/h2>/gi, "## $1\n");
|
|
82
|
+
md = md.replace(/<h3>(.*?)<\/h3>/gi, "### $1\n");
|
|
83
|
+
|
|
84
|
+
// Bold & Italic
|
|
85
|
+
md = md.replace(/<strong>(.*?)<\/strong>/gi, "**$1**");
|
|
86
|
+
md = md.replace(/<b>(.*?)<\/b>/gi, "**$1**");
|
|
87
|
+
md = md.replace(/<em>(.*?)<\/em>/gi, "*$1*");
|
|
88
|
+
md = md.replace(/<i>(.*?)<\/i>/gi, "*$1*");
|
|
89
|
+
|
|
90
|
+
// Blockquotes
|
|
91
|
+
md = md.replace(/<blockquote>(.*?)<\/blockquote>/gi, "> $1\n");
|
|
92
|
+
|
|
93
|
+
// Lists
|
|
94
|
+
md = md.replace(/<li>(.*?)<\/li>/gi, "- $1\n");
|
|
95
|
+
|
|
96
|
+
// Clean up any remaining HTML tags safely
|
|
97
|
+
md = md.replace(/<[^>]+>/g, "");
|
|
98
|
+
|
|
99
|
+
// Decode HTML entities
|
|
100
|
+
md = md
|
|
101
|
+
.replace(/&/g, "&")
|
|
102
|
+
.replace(/</g, "<")
|
|
103
|
+
.replace(/>/g, ">");
|
|
104
|
+
|
|
105
|
+
return md.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function MemoryPage({ activeBrainId }: { activeBrainId?: string }) {
|
|
109
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
110
|
+
const urlSlug = searchParams.get("slug")
|
|
111
|
+
|
|
112
|
+
const [activeTab, setActiveTab] = useState<"vault" | "conflicts" | "sessions">("vault")
|
|
113
|
+
const [nodes, setNodes] = useState<MemoryNode[]>([])
|
|
114
|
+
const [selected, setSelected] = useState<MemoryNode | null>(null)
|
|
115
|
+
const [category, setCategory] = useState("all")
|
|
116
|
+
const [query, setQuery] = useState("")
|
|
117
|
+
const [loading, setLoading] = useState(false)
|
|
118
|
+
const [error, setError] = useState("")
|
|
119
|
+
|
|
120
|
+
// Sessions state
|
|
121
|
+
const [sessionsList, setSessionsList] = useState<SessionSummary[]>([])
|
|
122
|
+
const [sessionsTotal, setSessionsTotal] = useState(0)
|
|
123
|
+
const [sessionsLoading, setSessionsLoading] = useState(false)
|
|
124
|
+
|
|
125
|
+
// Conflicts state
|
|
126
|
+
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
|
127
|
+
const [resolvingId, setResolvingId] = useState<string | null>(null)
|
|
128
|
+
|
|
129
|
+
// Rich inline editing states
|
|
130
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
131
|
+
const [isCreating, setIsCreating] = useState(false)
|
|
132
|
+
const [editSlug, setEditSlug] = useState("")
|
|
133
|
+
const [editTitle, setEditTitle] = useState("")
|
|
134
|
+
const [editCategory, setEditCategory] = useState("invariants")
|
|
135
|
+
const [editContent, setEditContent] = useState("")
|
|
136
|
+
const [editStatus, setEditStatus] = useState("active")
|
|
137
|
+
const [editPriority, setEditPriority] = useState("low")
|
|
138
|
+
const [editModality, setEditModality] = useState("should")
|
|
139
|
+
const [editConfidence, setEditConfidence] = useState(1.0)
|
|
140
|
+
const [editImportance, setEditImportance] = useState(3)
|
|
141
|
+
const [editTags, setEditTags] = useState("")
|
|
142
|
+
const [editRelated, setEditRelated] = useState("")
|
|
143
|
+
const [editContradicts, setEditContradicts] = useState("")
|
|
144
|
+
const [editSupersedes, setEditSupersedes] = useState("")
|
|
145
|
+
|
|
146
|
+
const editorRef = useRef<HTMLDivElement>(null)
|
|
147
|
+
|
|
148
|
+
const execCommand = (command: string, value: string = "") => {
|
|
149
|
+
document.execCommand(command, false, value)
|
|
150
|
+
editorRef.current?.focus()
|
|
151
|
+
if (editorRef.current) {
|
|
152
|
+
setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if ((isEditing || isCreating) && editorRef.current) {
|
|
158
|
+
editorRef.current.innerHTML = markdownToHtml(editContent)
|
|
159
|
+
}
|
|
160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- only sync editor HTML on toggle edit/create
|
|
161
|
+
}, [isEditing, isCreating])
|
|
162
|
+
|
|
163
|
+
const fetchNodes = useCallback(async () => {
|
|
164
|
+
setLoading(true)
|
|
165
|
+
setError("")
|
|
166
|
+
try {
|
|
167
|
+
if (query.trim()) {
|
|
168
|
+
const results = await searchMemory(query, category === "all" ? undefined : category)
|
|
169
|
+
setNodes(results)
|
|
170
|
+
} else {
|
|
171
|
+
const results = await listMemory(activeBrainId, category === "all" ? undefined : category)
|
|
172
|
+
setNodes(results)
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
setError((e as Error).message)
|
|
176
|
+
setNodes([])
|
|
177
|
+
} finally {
|
|
178
|
+
setLoading(false)
|
|
179
|
+
}
|
|
180
|
+
}, [category, query, activeBrainId])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (selected) {
|
|
185
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate form synchronization
|
|
186
|
+
setEditSlug(selected.slug || "")
|
|
187
|
+
setEditTitle(selected.title || "")
|
|
188
|
+
setEditCategory(selected.category || "invariants")
|
|
189
|
+
setEditContent(selected.content || selected.body || "")
|
|
190
|
+
setEditStatus(selected.status || "active")
|
|
191
|
+
setEditPriority(selected.priority || "low")
|
|
192
|
+
setEditModality(selected.modality || "should")
|
|
193
|
+
setEditConfidence(selected.confidence ?? 1.0)
|
|
194
|
+
setEditImportance(selected.importance ?? 3)
|
|
195
|
+
setEditTags((selected.tags || []).join(", "))
|
|
196
|
+
setEditRelated((selected.related || []).join(", "))
|
|
197
|
+
setEditContradicts((selected.contradicts || []).join(", "))
|
|
198
|
+
setEditSupersedes((selected.supersedes || []).join(", "))
|
|
199
|
+
}
|
|
200
|
+
}, [selected])
|
|
201
|
+
|
|
202
|
+
const handleSave = async () => {
|
|
203
|
+
if (!selected) return
|
|
204
|
+
setLoading(true)
|
|
205
|
+
setError("")
|
|
206
|
+
try {
|
|
207
|
+
const tagsArr = editTags.split(",").map(t => t.trim()).filter(Boolean)
|
|
208
|
+
const relatedArr = editRelated.split(",").map(t => t.trim()).filter(Boolean)
|
|
209
|
+
const contradictsArr = editContradicts.split(",").map(t => t.trim()).filter(Boolean)
|
|
210
|
+
const supersedesArr = editSupersedes.split(",").map(t => t.trim()).filter(Boolean)
|
|
211
|
+
|
|
212
|
+
const updated = await saveMemory(selected.slug, {
|
|
213
|
+
title: editTitle,
|
|
214
|
+
category: editCategory,
|
|
215
|
+
content: editContent,
|
|
216
|
+
status: editStatus,
|
|
217
|
+
priority: editPriority,
|
|
218
|
+
modality: editModality,
|
|
219
|
+
confidence: editConfidence,
|
|
220
|
+
importance: editImportance,
|
|
221
|
+
tags: tagsArr,
|
|
222
|
+
related: relatedArr,
|
|
223
|
+
contradicts: contradictsArr,
|
|
224
|
+
supersedes: supersedesArr
|
|
225
|
+
})
|
|
226
|
+
setSelected(updated)
|
|
227
|
+
setIsEditing(false)
|
|
228
|
+
fetchNodes()
|
|
229
|
+
} catch (e) {
|
|
230
|
+
setError((e as Error).message)
|
|
231
|
+
} finally {
|
|
232
|
+
setLoading(false)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const handleCreate = async () => {
|
|
237
|
+
if (!editSlug.trim() || !editTitle.trim() || !editContent.trim()) {
|
|
238
|
+
setError("Slug, Title, and Content are required fields.")
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
setLoading(true)
|
|
242
|
+
setError("")
|
|
243
|
+
try {
|
|
244
|
+
const tagsArr = editTags.split(",").map(t => t.trim()).filter(Boolean)
|
|
245
|
+
const relatedArr = editRelated.split(",").map(t => t.trim()).filter(Boolean)
|
|
246
|
+
const contradictsArr = editContradicts.split(",").map(t => t.trim()).filter(Boolean)
|
|
247
|
+
const supersedesArr = editSupersedes.split(",").map(t => t.trim()).filter(Boolean)
|
|
248
|
+
|
|
249
|
+
const created = await createMemory({
|
|
250
|
+
slug: editSlug,
|
|
251
|
+
title: editTitle,
|
|
252
|
+
category: editCategory,
|
|
253
|
+
content: editContent,
|
|
254
|
+
status: editStatus,
|
|
255
|
+
priority: editPriority,
|
|
256
|
+
modality: editModality,
|
|
257
|
+
confidence: editConfidence,
|
|
258
|
+
importance: editImportance,
|
|
259
|
+
tags: tagsArr,
|
|
260
|
+
related: relatedArr,
|
|
261
|
+
contradicts: contradictsArr,
|
|
262
|
+
supersedes: supersedesArr
|
|
263
|
+
})
|
|
264
|
+
setIsCreating(false)
|
|
265
|
+
setSelected(created)
|
|
266
|
+
setSearchParams({ slug: created.slug })
|
|
267
|
+
fetchNodes()
|
|
268
|
+
} catch (e) {
|
|
269
|
+
setError((e as Error).message)
|
|
270
|
+
} finally {
|
|
271
|
+
setLoading(false)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const handleDelete = async (slug: string) => {
|
|
276
|
+
if (!window.confirm(`Are you sure you want to delete memory node "${slug}"?`)) return
|
|
277
|
+
setLoading(true)
|
|
278
|
+
setError("")
|
|
279
|
+
try {
|
|
280
|
+
await deleteMemory(slug)
|
|
281
|
+
setSelected(null)
|
|
282
|
+
setSearchParams({})
|
|
283
|
+
fetchNodes()
|
|
284
|
+
} catch (e) {
|
|
285
|
+
setError((e as Error).message)
|
|
286
|
+
} finally {
|
|
287
|
+
setLoading(false)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const fetchConflictsList = useCallback(async () => {
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetchConflicts()
|
|
294
|
+
setConflicts(res.conflicts || [])
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.warn("Conflicts API error (enhanced features might be disabled):", e)
|
|
297
|
+
}
|
|
298
|
+
}, [])
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
const timer = setTimeout(fetchNodes, 300)
|
|
302
|
+
return () => clearTimeout(timer)
|
|
303
|
+
}, [fetchNodes])
|
|
304
|
+
|
|
305
|
+
const loadSessions = useCallback(async () => {
|
|
306
|
+
setSessionsLoading(true)
|
|
307
|
+
try {
|
|
308
|
+
const data = await fetchSessions(100)
|
|
309
|
+
setSessionsList(data.sessions)
|
|
310
|
+
setSessionsTotal(data.total)
|
|
311
|
+
} catch (e) {
|
|
312
|
+
setError((e as Error).message)
|
|
313
|
+
} finally {
|
|
314
|
+
setSessionsLoading(false)
|
|
315
|
+
}
|
|
316
|
+
}, [])
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (activeTab === "conflicts") {
|
|
321
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate tab data synchronization
|
|
322
|
+
fetchConflictsList()
|
|
323
|
+
} else if (activeTab === "sessions") {
|
|
324
|
+
loadSessions()
|
|
325
|
+
}
|
|
326
|
+
}, [activeTab, fetchConflictsList, loadSessions])
|
|
327
|
+
|
|
328
|
+
const handleDeleteSession = async (id: string) => {
|
|
329
|
+
if (!window.confirm(`Delete session "${id}"?`)) return
|
|
330
|
+
try {
|
|
331
|
+
await deleteSession(id)
|
|
332
|
+
loadSessions()
|
|
333
|
+
} catch (e) {
|
|
334
|
+
setError((e as Error).message)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (urlSlug) {
|
|
341
|
+
const loadUrlSlug = async () => {
|
|
342
|
+
setLoading(true)
|
|
343
|
+
setError("")
|
|
344
|
+
try {
|
|
345
|
+
const full = await readMemory(urlSlug)
|
|
346
|
+
setSelected(full)
|
|
347
|
+
} catch (e) {
|
|
348
|
+
setError(`Failed to load memory node '${urlSlug}': ${(e as Error).message}`)
|
|
349
|
+
setSelected(null)
|
|
350
|
+
} finally {
|
|
351
|
+
setLoading(false)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
loadUrlSlug()
|
|
355
|
+
} else {
|
|
356
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate reset on url change
|
|
357
|
+
setSelected(null)
|
|
358
|
+
}
|
|
359
|
+
}, [urlSlug])
|
|
360
|
+
|
|
361
|
+
// Wire SSE Live Agent Monitor Streaming (Port 9111)
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
const sseUrl = "http://localhost:9111/stream"
|
|
364
|
+
const eventSource = new EventSource(sseUrl)
|
|
365
|
+
|
|
366
|
+
eventSource.onmessage = (event) => {
|
|
367
|
+
try {
|
|
368
|
+
const logEntry = JSON.parse(event.data)
|
|
369
|
+
// If a conflict-detector log, memory ingestion, or surface rebuild log arrives, hot-refresh!
|
|
370
|
+
if (
|
|
371
|
+
logEntry.subsystem === "conflict-detector" ||
|
|
372
|
+
logEntry.subsystem === "rebuild" ||
|
|
373
|
+
logEntry.subsystem === "session-watcher"
|
|
374
|
+
) {
|
|
375
|
+
fetchNodes()
|
|
376
|
+
fetchConflictsList()
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// ignore
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
eventSource.onerror = () => {
|
|
384
|
+
eventSource.close()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return () => {
|
|
388
|
+
eventSource.close()
|
|
389
|
+
}
|
|
390
|
+
}, [fetchNodes, fetchConflictsList])
|
|
391
|
+
|
|
392
|
+
const handleSelect = (node: MemoryNode) => {
|
|
393
|
+
setSearchParams({ slug: node.slug })
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const handleResolveConflict = async (conflictId: string, action: "keep" | "supersede", winnerSlug: string) => {
|
|
397
|
+
setResolvingId(conflictId)
|
|
398
|
+
setError("")
|
|
399
|
+
try {
|
|
400
|
+
await resolveConflict(conflictId, action, winnerSlug)
|
|
401
|
+
await fetchConflictsList()
|
|
402
|
+
await fetchNodes()
|
|
403
|
+
} catch (e) {
|
|
404
|
+
setError((e as Error).message)
|
|
405
|
+
} finally {
|
|
406
|
+
setResolvingId(null)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const renderEditorForm = (onSubmit: () => void, onCancel: () => void, isNew: boolean) => {
|
|
411
|
+
return (
|
|
412
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
|
413
|
+
{/* Form fields */}
|
|
414
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
|
|
415
|
+
{/* Left Column */}
|
|
416
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
417
|
+
<div>
|
|
418
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Slug (kebab-case, matches filename)</label>
|
|
419
|
+
<input
|
|
420
|
+
type="text"
|
|
421
|
+
placeholder="e.g. prefer-atomic-writes"
|
|
422
|
+
value={editSlug}
|
|
423
|
+
onChange={e => setEditSlug(e.target.value)}
|
|
424
|
+
disabled={!isNew}
|
|
425
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
426
|
+
/>
|
|
427
|
+
</div>
|
|
428
|
+
<div>
|
|
429
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Title (One-line description)</label>
|
|
430
|
+
<input
|
|
431
|
+
type="text"
|
|
432
|
+
placeholder="e.g. Always write files atomically (write-then-rename)"
|
|
433
|
+
value={editTitle}
|
|
434
|
+
onChange={e => setEditTitle(e.target.value)}
|
|
435
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
436
|
+
/>
|
|
437
|
+
</div>
|
|
438
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
|
439
|
+
<div>
|
|
440
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Category</label>
|
|
441
|
+
<select
|
|
442
|
+
value={editCategory}
|
|
443
|
+
onChange={e => setEditCategory(e.target.value)}
|
|
444
|
+
style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
445
|
+
>
|
|
446
|
+
<option value="invariants">invariants</option>
|
|
447
|
+
<option value="patterns">patterns</option>
|
|
448
|
+
<option value="anti-patterns">anti-patterns</option>
|
|
449
|
+
<option value="preferences">preferences</option>
|
|
450
|
+
<option value="decisions">decisions</option>
|
|
451
|
+
<option value="concepts">concepts</option>
|
|
452
|
+
<option value="facts">facts</option>
|
|
453
|
+
<option value="lore">lore</option>
|
|
454
|
+
</select>
|
|
455
|
+
</div>
|
|
456
|
+
<div>
|
|
457
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Modality</label>
|
|
458
|
+
<select
|
|
459
|
+
value={editModality}
|
|
460
|
+
onChange={e => setEditModality(e.target.value)}
|
|
461
|
+
style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
462
|
+
>
|
|
463
|
+
<option value="must">must</option>
|
|
464
|
+
<option value="must_not">must_not</option>
|
|
465
|
+
<option value="should">should</option>
|
|
466
|
+
<option value="should_not">should_not</option>
|
|
467
|
+
</select>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
{/* Right Column */}
|
|
473
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
474
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
|
475
|
+
<div>
|
|
476
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Status</label>
|
|
477
|
+
<select
|
|
478
|
+
value={editStatus}
|
|
479
|
+
onChange={e => setEditStatus(e.target.value)}
|
|
480
|
+
style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
481
|
+
>
|
|
482
|
+
<option value="active">active</option>
|
|
483
|
+
<option value="draft">draft</option>
|
|
484
|
+
<option value="deprecated">deprecated</option>
|
|
485
|
+
<option value="superseded">superseded</option>
|
|
486
|
+
</select>
|
|
487
|
+
</div>
|
|
488
|
+
<div>
|
|
489
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Importance (1-5 Stars)</label>
|
|
490
|
+
<select
|
|
491
|
+
value={editImportance}
|
|
492
|
+
onChange={e => setEditImportance(Number(e.target.value))}
|
|
493
|
+
style={{ width: "100%", background: "rgba(15,23,42,0.9)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
494
|
+
>
|
|
495
|
+
<option value={1}>★ 1/5</option>
|
|
496
|
+
<option value={2}>★★ 2/5</option>
|
|
497
|
+
<option value={3}>★★★ 3/5</option>
|
|
498
|
+
<option value={4}>★★★★ 4/5</option>
|
|
499
|
+
<option value={5}>★★★★★ 5/5</option>
|
|
500
|
+
</select>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
<div>
|
|
504
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>
|
|
505
|
+
Confidence: <strong>{Math.round(editConfidence * 100)}%</strong>
|
|
506
|
+
</label>
|
|
507
|
+
<input
|
|
508
|
+
type="range"
|
|
509
|
+
min="0"
|
|
510
|
+
max="1"
|
|
511
|
+
step="0.05"
|
|
512
|
+
value={editConfidence}
|
|
513
|
+
onChange={e => setEditConfidence(Number(e.target.value))}
|
|
514
|
+
style={{ width: "100%", height: 6, background: "rgba(255,255,255,0.1)", borderRadius: 3, outline: "none" }}
|
|
515
|
+
/>
|
|
516
|
+
</div>
|
|
517
|
+
<div>
|
|
518
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Tags (comma separated)</label>
|
|
519
|
+
<input
|
|
520
|
+
type="text"
|
|
521
|
+
placeholder="e.g. filesystem, reliability, writes"
|
|
522
|
+
value={editTags}
|
|
523
|
+
onChange={e => setEditTags(e.target.value)}
|
|
524
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
{/* Semantic Relationships */}
|
|
531
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12 }}>
|
|
532
|
+
<div>
|
|
533
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Related Nodes (comma separated slugs)</label>
|
|
534
|
+
<input
|
|
535
|
+
type="text"
|
|
536
|
+
placeholder="e.g. use-atomic-writes, git-wiping"
|
|
537
|
+
value={editRelated}
|
|
538
|
+
onChange={e => setEditRelated(e.target.value)}
|
|
539
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
540
|
+
/>
|
|
541
|
+
</div>
|
|
542
|
+
<div>
|
|
543
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Contradicts Nodes (comma separated slugs)</label>
|
|
544
|
+
<input
|
|
545
|
+
type="text"
|
|
546
|
+
placeholder="e.g. write-anywhere-sync"
|
|
547
|
+
value={editContradicts}
|
|
548
|
+
onChange={e => setEditContradicts(e.target.value)}
|
|
549
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
550
|
+
/>
|
|
551
|
+
</div>
|
|
552
|
+
<div>
|
|
553
|
+
<label style={{ display: "block", fontSize: 12, color: "var(--text-secondary)", marginBottom: 4 }}>Supersedes Nodes (comma separated slugs)</label>
|
|
554
|
+
<input
|
|
555
|
+
type="text"
|
|
556
|
+
placeholder="e.g. prefer-atomic-writes-v1"
|
|
557
|
+
value={editSupersedes}
|
|
558
|
+
onChange={e => setEditSupersedes(e.target.value)}
|
|
559
|
+
style={{ width: "100%", background: "rgba(255,255,255,0.03)", border: "1px solid var(--border)", borderRadius: 6, padding: "8px 12px", color: "#fff" }}
|
|
560
|
+
/>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
{/* WYSIWYG visual HTML Editor - NO RAW CODE PREVIEW! EDIT Styled preview DIRECTLY */}
|
|
565
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
566
|
+
<label style={{ fontSize: 12, color: "var(--text-secondary)" }}>Visual WYSIWYG Memory Content Editor</label>
|
|
567
|
+
<div style={{ background: "rgba(10,15,30,0.5)", border: "1px solid var(--border)", borderRadius: 8, overflow: "hidden" }}>
|
|
568
|
+
{/* Rich Formatting Toolbar */}
|
|
569
|
+
<div style={{ display: "flex", gap: 8, padding: 8, background: "rgba(255,255,255,0.03)", borderBottom: "1px solid var(--border)", flexWrap: "wrap" }}>
|
|
570
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("bold")} style={{ fontWeight: "bold", padding: "4px 10px" }}>B</button>
|
|
571
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("italic")} style={{ fontStyle: "italic", padding: "4px 10px" }}>I</button>
|
|
572
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h1>")} style={{ padding: "4px 8px" }}>H1</button>
|
|
573
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<h2>")} style={{ padding: "4px 8px" }}>H2</button>
|
|
574
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("formatBlock", "<blockquote>")} style={{ padding: "4px 8px" }}>“ Quote</button>
|
|
575
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("insertUnorderedList")} style={{ padding: "4px 8px" }}>• List</button>
|
|
576
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => execCommand("removeFormat")} style={{ padding: "4px 8px", fontSize: 11 }}>Clear</button>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
{/* Direct contentEditable Editor Pane */}
|
|
580
|
+
<div
|
|
581
|
+
ref={editorRef}
|
|
582
|
+
contentEditable
|
|
583
|
+
onInput={() => {
|
|
584
|
+
if (editorRef.current) {
|
|
585
|
+
setEditContent(htmlToMarkdown(editorRef.current.innerHTML))
|
|
586
|
+
}
|
|
587
|
+
}}
|
|
588
|
+
style={{
|
|
589
|
+
minHeight: 300,
|
|
590
|
+
padding: 16,
|
|
591
|
+
color: "#fff",
|
|
592
|
+
background: "#090d16",
|
|
593
|
+
outline: "none",
|
|
594
|
+
fontSize: 14,
|
|
595
|
+
lineHeight: 1.6,
|
|
596
|
+
overflowY: "auto"
|
|
597
|
+
}}
|
|
598
|
+
/>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
{/* Action Buttons */}
|
|
603
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: 12, marginTop: 12 }}>
|
|
604
|
+
<button className="btn btn-ghost" type="button" onClick={onCancel} style={{ fontSize: 13 }}>Cancel</button>
|
|
605
|
+
<button className="btn btn-purple" type="button" onClick={onSubmit} style={{ fontSize: 13 }}>
|
|
606
|
+
{isNew ? "🚀 Create Memory Node" : "💾 Save Changes"}
|
|
607
|
+
</button>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const categoryCounts = nodes.reduce<Record<string, number>>((acc, n) => {
|
|
614
|
+
acc[n.category] = (acc[n.category] || 0) + 1
|
|
615
|
+
return acc
|
|
616
|
+
}, {})
|
|
617
|
+
|
|
618
|
+
return (
|
|
619
|
+
<div className="page">
|
|
620
|
+
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 20 }}>
|
|
621
|
+
<div>
|
|
622
|
+
<h1>Memory Vault</h1>
|
|
623
|
+
<p>Browse, audit, and resolve contradictions in your SSSS knowledge graph</p>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* Tab Selection */}
|
|
627
|
+
<div style={{ display: "flex", background: "rgba(255,255,255,0.03)", padding: 4, borderRadius: 8, border: "1px solid var(--border)" }}>
|
|
628
|
+
<button
|
|
629
|
+
className={`btn btn-sm ${activeTab === "vault" ? "btn-purple" : "btn-ghost"}`}
|
|
630
|
+
onClick={() => { setActiveTab("vault"); setSelected(null); setSearchParams({}); }}
|
|
631
|
+
style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px" }}
|
|
632
|
+
>
|
|
633
|
+
🧠 Active Nodes
|
|
634
|
+
</button>
|
|
635
|
+
<button
|
|
636
|
+
className={`btn btn-sm ${activeTab === "conflicts" ? "btn-cyan" : "btn-ghost"}`}
|
|
637
|
+
onClick={() => { setActiveTab("conflicts"); setSelected(null); setSearchParams({}); }}
|
|
638
|
+
style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px", display: "flex", alignItems: "center", gap: 6 }}
|
|
639
|
+
>
|
|
640
|
+
⚠️ Quarantined Conflicts
|
|
641
|
+
{conflicts.length > 0 && (
|
|
642
|
+
<span style={{ background: "#ef4444", color: "#fff", fontSize: 9, padding: "1px 5px", borderRadius: 10, fontWeight: 700 }}>
|
|
643
|
+
{conflicts.length}
|
|
644
|
+
</span>
|
|
645
|
+
)}
|
|
646
|
+
</button>
|
|
647
|
+
<button
|
|
648
|
+
className={`btn btn-sm ${activeTab === "sessions" ? "btn-purple" : "btn-ghost"}`}
|
|
649
|
+
onClick={() => { setActiveTab("sessions"); setSelected(null); setSearchParams({}); }}
|
|
650
|
+
style={{ borderRadius: 6, fontSize: 12, padding: "6px 12px" }}
|
|
651
|
+
>
|
|
652
|
+
📝 Sessions
|
|
653
|
+
{sessionsTotal > 0 && (
|
|
654
|
+
<span style={{ background: "rgba(139,92,246,0.3)", color: "#a78bfa", fontSize: 9, padding: "1px 5px", borderRadius: 10, fontWeight: 700, marginLeft: 4 }}>
|
|
655
|
+
{sessionsTotal}
|
|
656
|
+
</span>
|
|
657
|
+
)}
|
|
658
|
+
</button>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
{error && <div className="badge badge-error" style={{ alignSelf: "flex-start", marginBottom: 15 }}>⚠️ {error}</div>}
|
|
663
|
+
|
|
664
|
+
{activeTab === "vault" ? (
|
|
665
|
+
<div className="memory-layout">
|
|
666
|
+
<div className="memory-sidebar">
|
|
667
|
+
<h3 style={{ margin: "0 0 12px 0" }}>Categories</h3>
|
|
668
|
+
{CATEGORIES.map(c => (
|
|
669
|
+
<button
|
|
670
|
+
key={c}
|
|
671
|
+
className={`category-btn ${category === c ? "active" : ""}`}
|
|
672
|
+
onClick={() => { setCategory(c); setSearchParams({}); setIsCreating(false); setIsEditing(false); }}
|
|
673
|
+
>
|
|
674
|
+
<span>{c === "all" ? "📋 All Nodes" : c}</span>
|
|
675
|
+
{c === "all"
|
|
676
|
+
? <span className="category-count">{nodes.length}</span>
|
|
677
|
+
: categoryCounts[c]
|
|
678
|
+
? <span className="category-count">{categoryCounts[c]}</span>
|
|
679
|
+
: null
|
|
680
|
+
}
|
|
681
|
+
</button>
|
|
682
|
+
))}
|
|
683
|
+
|
|
684
|
+
<button
|
|
685
|
+
className="btn btn-purple"
|
|
686
|
+
style={{ width: "100%", marginTop: 20, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, fontSize: 13 }}
|
|
687
|
+
onClick={() => {
|
|
688
|
+
setSelected(null);
|
|
689
|
+
setSearchParams({});
|
|
690
|
+
setIsCreating(true);
|
|
691
|
+
setIsEditing(false);
|
|
692
|
+
setEditSlug("");
|
|
693
|
+
setEditTitle("");
|
|
694
|
+
setEditCategory("invariants");
|
|
695
|
+
setEditContent("");
|
|
696
|
+
setEditStatus("active");
|
|
697
|
+
setEditPriority("low");
|
|
698
|
+
setEditModality("should");
|
|
699
|
+
setEditConfidence(1.0);
|
|
700
|
+
setEditImportance(3);
|
|
701
|
+
setEditTags("");
|
|
702
|
+
setEditRelated("");
|
|
703
|
+
setEditContradicts("");
|
|
704
|
+
setEditSupersedes("");
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
➕ Create New Node
|
|
708
|
+
</button>
|
|
709
|
+
</div>
|
|
710
|
+
<div className="memory-main">
|
|
711
|
+
<div className="memory-search">
|
|
712
|
+
<input
|
|
713
|
+
id="memory-search"
|
|
714
|
+
type="text"
|
|
715
|
+
placeholder="Search memory nodes…"
|
|
716
|
+
value={query}
|
|
717
|
+
onChange={e => setQuery(e.target.value)}
|
|
718
|
+
/>
|
|
719
|
+
<button className="btn btn-ghost btn-sm" onClick={fetchNodes}>Refresh</button>
|
|
720
|
+
</div>
|
|
721
|
+
{loading ? (
|
|
722
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
723
|
+
{[1, 2, 3].map(i => <div key={i} className="skeleton" style={{ height: 60 }} />)}
|
|
724
|
+
</div>
|
|
725
|
+
) : isCreating ? (
|
|
726
|
+
<div className="memory-detail" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
|
|
727
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
|
|
728
|
+
<h2 style={{ margin: 0, color: "#fff" }}>Create New SSSS Memory Node</h2>
|
|
729
|
+
<button className="btn btn-ghost btn-sm" onClick={() => setIsCreating(false)} style={{ fontSize: 12 }}>← Back to List</button>
|
|
730
|
+
</div>
|
|
731
|
+
{renderEditorForm(handleCreate, () => setIsCreating(false), true)}
|
|
732
|
+
</div>
|
|
733
|
+
) : selected && isEditing ? (
|
|
734
|
+
<div className="memory-detail" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
|
|
735
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 20 }}>
|
|
736
|
+
<h2 style={{ margin: 0, color: "#fff" }}>Edit Memory Node</h2>
|
|
737
|
+
<button className="btn btn-ghost btn-sm" onClick={() => setIsEditing(false)} style={{ fontSize: 12 }}>← Cancel Edit</button>
|
|
738
|
+
</div>
|
|
739
|
+
{renderEditorForm(handleSave, () => setIsEditing(false), false)}
|
|
740
|
+
</div>
|
|
741
|
+
) : selected ? (
|
|
742
|
+
<div className="memory-detail" style={{
|
|
743
|
+
background: "rgba(15,23,42,0.45)",
|
|
744
|
+
backdropFilter: "blur(12px)",
|
|
745
|
+
border: selected.modality === "must"
|
|
746
|
+
? "2px solid #10b981"
|
|
747
|
+
: selected.modality === "must_not"
|
|
748
|
+
? "2px solid #ef4444"
|
|
749
|
+
: selected.modality === "should"
|
|
750
|
+
? "1px solid #8b5cf6"
|
|
751
|
+
: selected.modality === "should_not"
|
|
752
|
+
? "1px solid #f97316"
|
|
753
|
+
: "1px solid var(--border)",
|
|
754
|
+
boxShadow: selected.modality === "must"
|
|
755
|
+
? "0 0 16px rgba(16, 185, 129, 0.1)"
|
|
756
|
+
: selected.modality === "must_not"
|
|
757
|
+
? "0 0 16px rgba(239, 68, 68, 0.1)"
|
|
758
|
+
: undefined,
|
|
759
|
+
borderRadius: 12,
|
|
760
|
+
padding: 24,
|
|
761
|
+
}}>
|
|
762
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16 }}>
|
|
763
|
+
<button className="btn btn-ghost btn-sm" onClick={() => setSearchParams({})} style={{ fontSize: 12 }}>← Back to List</button>
|
|
764
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
765
|
+
<button
|
|
766
|
+
className="btn btn-ghost btn-sm"
|
|
767
|
+
onClick={() => {
|
|
768
|
+
setEditSlug(selected.slug || "");
|
|
769
|
+
setEditTitle(selected.title || "");
|
|
770
|
+
setEditCategory(selected.category || "invariants");
|
|
771
|
+
setEditContent(selected.content || selected.body || "");
|
|
772
|
+
setEditStatus(selected.status || "active");
|
|
773
|
+
setEditPriority(selected.priority || "low");
|
|
774
|
+
setEditModality(selected.modality || "should");
|
|
775
|
+
setEditConfidence(selected.confidence ?? 1.0);
|
|
776
|
+
setEditImportance(selected.importance ?? 3);
|
|
777
|
+
setEditTags((selected.tags || []).join(", "));
|
|
778
|
+
setEditRelated((selected.related || []).join(", "));
|
|
779
|
+
setEditContradicts((selected.contradicts || []).join(", "));
|
|
780
|
+
setEditSupersedes((selected.supersedes || []).join(", "));
|
|
781
|
+
setIsEditing(true);
|
|
782
|
+
}}
|
|
783
|
+
style={{ fontSize: 12, border: "1px solid var(--border)" }}
|
|
784
|
+
>
|
|
785
|
+
✏️ Edit Node
|
|
786
|
+
</button>
|
|
787
|
+
<button
|
|
788
|
+
className="btn btn-ghost btn-sm"
|
|
789
|
+
onClick={() => handleDelete(selected.slug)}
|
|
790
|
+
style={{ fontSize: 12, borderColor: "rgba(239,68,68,0.2)", color: "#fca5a5" }}
|
|
791
|
+
>
|
|
792
|
+
🗑️ Delete
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
|
|
798
|
+
<h2 style={{ margin: 0, color: "#fff" }}>{selected.title}</h2>
|
|
799
|
+
{selected.modality && (
|
|
800
|
+
<span style={{
|
|
801
|
+
fontSize: 10,
|
|
802
|
+
fontWeight: 700,
|
|
803
|
+
textTransform: "uppercase",
|
|
804
|
+
padding: "4px 10px",
|
|
805
|
+
borderRadius: 6,
|
|
806
|
+
background: selected.modality === "must"
|
|
807
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
808
|
+
: selected.modality === "must_not"
|
|
809
|
+
? "rgba(239, 68, 68, 0.15)"
|
|
810
|
+
: "rgba(255, 255, 255, 0.05)",
|
|
811
|
+
color: selected.modality === "must"
|
|
812
|
+
? "#10b981"
|
|
813
|
+
: selected.modality === "must_not"
|
|
814
|
+
? "#ef4444"
|
|
815
|
+
: "var(--text-secondary)",
|
|
816
|
+
border: selected.modality === "must"
|
|
817
|
+
? "1px solid rgba(16, 185, 129, 0.3)"
|
|
818
|
+
: selected.modality === "must_not"
|
|
819
|
+
? "1px solid rgba(239, 68, 68, 0.3)"
|
|
820
|
+
: "1px solid rgba(255, 255, 255, 0.1)"
|
|
821
|
+
}}>
|
|
822
|
+
{selected.modality}
|
|
823
|
+
</span>
|
|
824
|
+
)}
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<dl className="frontmatter" style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: "10px 20px", marginBottom: 24, paddingBottom: 16, borderBottom: "1px solid rgba(255,255,255,0.06)" }}>
|
|
828
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Slug</dt>
|
|
829
|
+
<dd style={{ margin: 0 }}><code>{selected.slug}</code></dd>
|
|
830
|
+
|
|
831
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Category</dt>
|
|
832
|
+
<dd style={{ margin: 0, textTransform: "capitalize" }}>{selected.category}</dd>
|
|
833
|
+
|
|
834
|
+
{selected.status && (
|
|
835
|
+
<>
|
|
836
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Status</dt>
|
|
837
|
+
<dd style={{ margin: 0 }}>
|
|
838
|
+
<span className="badge badge-success" style={{ background: selected.status === "active" ? "rgba(34, 197, 94, 0.15)" : "rgba(255,255,255,0.05)", color: selected.status === "active" ? "#22c55e" : "var(--text-secondary)" }}>
|
|
839
|
+
{selected.status}
|
|
840
|
+
</span>
|
|
841
|
+
</dd>
|
|
842
|
+
</>
|
|
843
|
+
)}
|
|
844
|
+
|
|
845
|
+
{selected.confidence !== undefined && (
|
|
846
|
+
<>
|
|
847
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Confidence</dt>
|
|
848
|
+
<dd style={{ margin: 0, display: "flex", alignItems: "center", gap: 12 }}>
|
|
849
|
+
<strong style={{ fontSize: 13 }}>{Math.round(selected.confidence * 100)}%</strong>
|
|
850
|
+
<div style={{ flex: 1, maxWidth: 150, height: 6, background: "rgba(255,255,255,0.06)", borderRadius: 3, overflow: "hidden", display: "inline-block" }}>
|
|
851
|
+
<div style={{ width: `${Math.round(selected.confidence * 100)}%`, height: "100%", background: "linear-gradient(90deg, #8b5cf6, #06b6d4)", borderRadius: 3 }} />
|
|
852
|
+
</div>
|
|
853
|
+
</dd>
|
|
854
|
+
</>
|
|
855
|
+
)}
|
|
856
|
+
|
|
857
|
+
{selected.importance !== undefined && (
|
|
858
|
+
<>
|
|
859
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Importance</dt>
|
|
860
|
+
<dd style={{ margin: 0, color: "#fbbf24", display: "flex", gap: 2 }}>
|
|
861
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
862
|
+
<span key={i}>{i < (selected.importance || 0) ? "★" : "☆"}</span>
|
|
863
|
+
))}
|
|
864
|
+
</dd>
|
|
865
|
+
</>
|
|
866
|
+
)}
|
|
867
|
+
|
|
868
|
+
{selected.tags?.length ? (
|
|
869
|
+
<>
|
|
870
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Tags</dt>
|
|
871
|
+
<dd style={{ margin: 0, display: "flex", gap: 6, flexWrap: "wrap" }}>
|
|
872
|
+
{selected.tags.map(t => (
|
|
873
|
+
<span key={t} style={{ background: "rgba(255,255,255,0.05)", color: "var(--text-secondary)", fontSize: 10, padding: "2px 8px", borderRadius: 4, border: "1px solid rgba(255,255,255,0.03)" }}>
|
|
874
|
+
{t}
|
|
875
|
+
</span>
|
|
876
|
+
))}
|
|
877
|
+
</dd>
|
|
878
|
+
</>
|
|
879
|
+
) : null}
|
|
880
|
+
|
|
881
|
+
{selected.related?.length ? (
|
|
882
|
+
<>
|
|
883
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Related</dt>
|
|
884
|
+
<dd style={{ margin: 0 }}><code>{selected.related.join(", ")}</code></dd>
|
|
885
|
+
</>
|
|
886
|
+
) : null}
|
|
887
|
+
|
|
888
|
+
{selected.contradicts?.length ? (
|
|
889
|
+
<>
|
|
890
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Contradicts</dt>
|
|
891
|
+
<dd style={{ margin: 0, color: "#fca5a5" }}><code>{selected.contradicts.join(", ")}</code></dd>
|
|
892
|
+
</>
|
|
893
|
+
) : null}
|
|
894
|
+
|
|
895
|
+
{selected.supersedes?.length ? (
|
|
896
|
+
<>
|
|
897
|
+
<dt style={{ color: "var(--text-tertiary)", fontWeight: 500 }}>Supersedes</dt>
|
|
898
|
+
<dd style={{ margin: 0, color: "#93c5fd" }}><code>{selected.supersedes.join(", ")}</code></dd>
|
|
899
|
+
</>
|
|
900
|
+
) : null}
|
|
901
|
+
</dl>
|
|
902
|
+
|
|
903
|
+
<div style={{ marginTop: 20 }}>
|
|
904
|
+
<h3 style={{ fontSize: 12, textTransform: "uppercase", color: "var(--text-tertiary)", letterSpacing: 0.5, marginBottom: 12 }}>Memory Content (SSSS Body)</h3>
|
|
905
|
+
<div style={{ background: "rgba(0,0,0,0.15)", border: "1px solid rgba(255,255,255,0.04)", borderRadius: 8, padding: 20 }}>
|
|
906
|
+
{renderMarkdown(selected.body || selected.content || "")}
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
) : (
|
|
911
|
+
<div className="memory-list" style={{ display: "grid", gridTemplateColumns: "1fr", gap: 12 }}>
|
|
912
|
+
{nodes.length === 0 && !loading && (
|
|
913
|
+
<div style={{ textAlign: "center", padding: 40, color: "var(--text-tertiary)" }}>
|
|
914
|
+
{query ? "No matching nodes found." : "No memory nodes loaded. Is the backend running?"}
|
|
915
|
+
</div>
|
|
916
|
+
)}
|
|
917
|
+
{nodes.map(n => {
|
|
918
|
+
const borderStyle = n.modality === "must"
|
|
919
|
+
? "2px solid #10b981"
|
|
920
|
+
: n.modality === "must_not"
|
|
921
|
+
? "2px solid #ef4444"
|
|
922
|
+
: n.modality === "should"
|
|
923
|
+
? "1px solid #8b5cf6"
|
|
924
|
+
: n.modality === "should_not"
|
|
925
|
+
? "1px solid #f97316"
|
|
926
|
+
: "1px solid rgba(255, 255, 255, 0.08)";
|
|
927
|
+
|
|
928
|
+
const glowStyle = n.modality === "must"
|
|
929
|
+
? "0 0 12px rgba(16, 185, 129, 0.15)"
|
|
930
|
+
: n.modality === "must_not"
|
|
931
|
+
? "0 0 12px rgba(239, 68, 68, 0.15)"
|
|
932
|
+
: undefined;
|
|
933
|
+
|
|
934
|
+
return (
|
|
935
|
+
<div
|
|
936
|
+
key={n.slug}
|
|
937
|
+
className="memory-node-card"
|
|
938
|
+
onClick={() => handleSelect(n)}
|
|
939
|
+
style={{
|
|
940
|
+
border: borderStyle,
|
|
941
|
+
boxShadow: glowStyle,
|
|
942
|
+
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
943
|
+
position: "relative"
|
|
944
|
+
}}
|
|
945
|
+
>
|
|
946
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 6 }}>
|
|
947
|
+
<h4 style={{ margin: 0, color: "#fff", fontSize: 15 }}>{n.title}</h4>
|
|
948
|
+
{n.modality && (
|
|
949
|
+
<span style={{
|
|
950
|
+
fontSize: 9,
|
|
951
|
+
fontWeight: 700,
|
|
952
|
+
textTransform: "uppercase",
|
|
953
|
+
padding: "2px 6px",
|
|
954
|
+
borderRadius: 4,
|
|
955
|
+
background: n.modality === "must"
|
|
956
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
957
|
+
: n.modality === "must_not"
|
|
958
|
+
? "rgba(239, 68, 68, 0.15)"
|
|
959
|
+
: "rgba(255, 255, 255, 0.05)",
|
|
960
|
+
color: n.modality === "must"
|
|
961
|
+
? "#10b981"
|
|
962
|
+
: n.modality === "must_not"
|
|
963
|
+
? "#ef4444"
|
|
964
|
+
: "var(--text-secondary)"
|
|
965
|
+
}}>
|
|
966
|
+
{n.modality}
|
|
967
|
+
</span>
|
|
968
|
+
)}
|
|
969
|
+
</div>
|
|
970
|
+
<div style={{ display: "flex", flexWrap: "wrap", gap: 16, fontSize: 11, color: "var(--text-tertiary)", marginTop: 8 }}>
|
|
971
|
+
<span>📂 {n.category}</span>
|
|
972
|
+
{n.importance !== undefined && (
|
|
973
|
+
<span style={{ color: "#fbbf24" }}>★ {n.importance}/5</span>
|
|
974
|
+
)}
|
|
975
|
+
{n.confidence !== undefined && (
|
|
976
|
+
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
|
|
977
|
+
🧠 {Math.round(n.confidence * 100)}%
|
|
978
|
+
</span>
|
|
979
|
+
)}
|
|
980
|
+
</div>
|
|
981
|
+
</div>
|
|
982
|
+
);
|
|
983
|
+
})}
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
</div>
|
|
987
|
+
</div>
|
|
988
|
+
) : (
|
|
989
|
+
// SSE-driven Quarantined Conflicts override panel
|
|
990
|
+
<div style={{ width: "100%" }}>
|
|
991
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
|
992
|
+
<h3>Quarantined Contradiction Records</h3>
|
|
993
|
+
<span style={{ fontSize: 12, color: "var(--text-tertiary)", display: "flex", alignItems: "center", gap: 5 }}>
|
|
994
|
+
<span className="pulse" style={{ background: "#06b6d4" }} /> SSE Real-time Agent Monitor Connected
|
|
995
|
+
</span>
|
|
996
|
+
</div>
|
|
997
|
+
|
|
998
|
+
{conflicts.length === 0 ? (
|
|
999
|
+
<div style={{ textAlign: "center", padding: 60, background: "rgba(255,255,255,0.02)", borderRadius: 12, border: "1px dashed var(--border)", color: "var(--text-tertiary)" }}>
|
|
1000
|
+
<span style={{ fontSize: 32, display: "block", marginBottom: 12 }}>🛡️</span>
|
|
1001
|
+
<h4 style={{ color: "#fff", marginBottom: 6 }}>All Clear!</h4>
|
|
1002
|
+
No memory conflicts enqueued or quarantined in `.agent/memory-inbox/conflicts/`.
|
|
1003
|
+
</div>
|
|
1004
|
+
) : (
|
|
1005
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
|
1006
|
+
{conflicts.map((c) => {
|
|
1007
|
+
const isQuarantined = c.status === "pending";
|
|
1008
|
+
|
|
1009
|
+
return (
|
|
1010
|
+
<div
|
|
1011
|
+
key={c.conflict_id}
|
|
1012
|
+
style={{
|
|
1013
|
+
background: "rgba(15,23,42,0.45)",
|
|
1014
|
+
backdropFilter: "blur(12px)",
|
|
1015
|
+
border: isQuarantined ? "1px solid rgba(239, 68, 68, 0.25)" : "1px solid rgba(34, 197, 94, 0.2)",
|
|
1016
|
+
borderRadius: 12,
|
|
1017
|
+
padding: 24,
|
|
1018
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.15)"
|
|
1019
|
+
}}
|
|
1020
|
+
>
|
|
1021
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 16, borderBottom: "1px solid rgba(255,255,255,0.06)", paddingBottom: 12 }}>
|
|
1022
|
+
<div>
|
|
1023
|
+
<h4 style={{ margin: 0, fontSize: 16, color: "#fff", display: "flex", alignItems: "center", gap: 8 }}>
|
|
1024
|
+
Conflict: <code>{c.conflict_id}</code>
|
|
1025
|
+
<span style={{ fontSize: 11, padding: "2px 8px", borderRadius: 10, background: isQuarantined ? "rgba(239,68,68,0.15)" : "rgba(34,197,94,0.15)", color: isQuarantined ? "#ef4444" : "#22c55e", textTransform: "uppercase" }}>
|
|
1026
|
+
{c.status}
|
|
1027
|
+
</span>
|
|
1028
|
+
</h4>
|
|
1029
|
+
<span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Detected at: {new Date(c.detected_at).toLocaleString()}</span>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div style={{ textAlign: "right" }}>
|
|
1032
|
+
<span style={{ fontSize: 12, color: "#ef4444", fontWeight: 700, display: "block" }}>
|
|
1033
|
+
Clash Similarity: {Math.round(c.similarity * 100)}%
|
|
1034
|
+
</span>
|
|
1035
|
+
<span style={{ fontSize: 10, color: "var(--text-tertiary)" }}>Modality polarity flip</span>
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
|
|
1039
|
+
<p style={{ fontSize: 13, background: "rgba(239,68,68,0.03)", padding: "10px 14px", borderRadius: 6, borderLeft: "3px solid #ef4444", color: "#fca5a5", marginBottom: 20 }}>
|
|
1040
|
+
<strong>Reason:</strong> {c.reason}
|
|
1041
|
+
</p>
|
|
1042
|
+
|
|
1043
|
+
{/* Nodes Comparison Table */}
|
|
1044
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 24 }}>
|
|
1045
|
+
{/* Existing Node */}
|
|
1046
|
+
<div style={{ background: "rgba(255,255,255,0.02)", padding: 16, borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)" }}>
|
|
1047
|
+
<h5 style={{ margin: "0 0 10px 0", color: "#a78bfa", textTransform: "uppercase", fontSize: 11, letterSpacing: 0.5 }}>✓ Existing Node In Vault</h5>
|
|
1048
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: "#fff", marginBottom: 8 }}><code>{c.existing_slug}</code></div>
|
|
1049
|
+
<dl style={{ display: "grid", gridTemplateColumns: "80px 1fr", gap: 4, margin: "0 0 12px 0", fontSize: 11, color: "var(--text-secondary)" }}>
|
|
1050
|
+
<dt>Modality:</dt><dd><code>{c._existing_node?.modality || "must"}</code></dd>
|
|
1051
|
+
<dt>Polarity:</dt><dd><code>{c._existing_node?.sentiment_polarity || "directive_must"}</code></dd>
|
|
1052
|
+
<dt>Confidence:</dt><dd>{c._existing_node?.confidence !== undefined ? `${Math.round(c._existing_node.confidence * 100)}%` : "N/A"}</dd>
|
|
1053
|
+
</dl>
|
|
1054
|
+
<div style={{ fontSize: 12, fontStyle: "italic", background: "rgba(0,0,0,0.2)", padding: 10, borderRadius: 6, color: "var(--text-tertiary)", minHeight: 60, whiteSpace: "pre-wrap" }}>
|
|
1055
|
+
{c._existing_node?.excerpt || c._existing_node?.body || "No preview content."}
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
|
|
1059
|
+
{/* Candidate Node */}
|
|
1060
|
+
<div style={{ background: "rgba(255,255,255,0.02)", padding: 16, borderRadius: 8, border: "1px solid rgba(255,255,255,0.05)" }}>
|
|
1061
|
+
<h5 style={{ margin: "0 0 10px 0", color: "#06b6d4", textTransform: "uppercase", fontSize: 11, letterSpacing: 0.5 }}>⚠ New Ingestion Candidate</h5>
|
|
1062
|
+
<div style={{ fontSize: 13, fontWeight: 700, color: "#fff", marginBottom: 8 }}><code>{c.new_slug}</code></div>
|
|
1063
|
+
<dl style={{ display: "grid", gridTemplateColumns: "80px 1fr", gap: 4, margin: "0 0 12px 0", fontSize: 11, color: "var(--text-secondary)" }}>
|
|
1064
|
+
<dt>Modality:</dt><dd><code>{c._new_node?.modality || "must_not"}</code></dd>
|
|
1065
|
+
<dt>Polarity:</dt><dd><code>{c._new_node?.sentiment_polarity || "directive_must_not"}</code></dd>
|
|
1066
|
+
<dt>Confidence:</dt><dd>{c._new_node?.confidence !== undefined ? `${Math.round(c._new_node.confidence * 100)}%` : "N/A"}</dd>
|
|
1067
|
+
</dl>
|
|
1068
|
+
<div style={{ fontSize: 12, fontStyle: "italic", background: "rgba(0,0,0,0.2)", padding: 10, borderRadius: 6, color: "var(--text-tertiary)", minHeight: 60, whiteSpace: "pre-wrap" }}>
|
|
1069
|
+
{c._new_node?.excerpt || c._new_node?.body || "No preview content."}
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
</div>
|
|
1073
|
+
|
|
1074
|
+
{/* Resolution Actions */}
|
|
1075
|
+
{isQuarantined ? (
|
|
1076
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: 12 }}>
|
|
1077
|
+
<button
|
|
1078
|
+
className="btn btn-ghost btn-sm"
|
|
1079
|
+
disabled={resolvingId !== null}
|
|
1080
|
+
onClick={() => handleResolveConflict(c.conflict_id, "keep", c.existing_slug)}
|
|
1081
|
+
style={{ borderColor: "rgba(255,255,255,0.1)", fontSize: 12 }}
|
|
1082
|
+
>
|
|
1083
|
+
{resolvingId === c.conflict_id ? "Processing..." : `Keep Existing Node (${c.existing_slug})`}
|
|
1084
|
+
</button>
|
|
1085
|
+
<button
|
|
1086
|
+
className="btn btn-purple btn-sm"
|
|
1087
|
+
disabled={resolvingId !== null}
|
|
1088
|
+
onClick={() => handleResolveConflict(c.conflict_id, "supersede", c.new_slug)}
|
|
1089
|
+
style={{ fontSize: 12 }}
|
|
1090
|
+
>
|
|
1091
|
+
{resolvingId === c.conflict_id ? "Processing..." : `Supersede with New (${c.new_slug})`}
|
|
1092
|
+
</button>
|
|
1093
|
+
</div>
|
|
1094
|
+
) : (
|
|
1095
|
+
<div style={{ fontSize: 12, color: "#22c55e", background: "rgba(34,197,94,0.06)", padding: "10px 14px", borderRadius: 6, border: "1px solid rgba(34,197,94,0.15)", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
1096
|
+
<span>✓ Resolved successfully. Choice: <strong>{c.resolution}</strong></span>
|
|
1097
|
+
<span style={{ fontSize: 11, color: "var(--text-tertiary)" }}>Resolved at: {c.resolved_at ? new Date(c.resolved_at).toLocaleString() : ""}</span>
|
|
1098
|
+
</div>
|
|
1099
|
+
)}
|
|
1100
|
+
</div>
|
|
1101
|
+
)
|
|
1102
|
+
})}
|
|
1103
|
+
</div>
|
|
1104
|
+
)}
|
|
1105
|
+
</div>
|
|
1106
|
+
)}
|
|
1107
|
+
|
|
1108
|
+
{activeTab === "sessions" && (
|
|
1109
|
+
<div className="health-log" style={{ background: "rgba(15,23,42,0.45)", backdropFilter: "blur(12px)", border: "1px solid var(--border)", borderRadius: 12, padding: 24 }}>
|
|
1110
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
|
|
1111
|
+
<h2 style={{ margin: 0, color: "#fff", fontSize: 16 }}>📝 IDE Sessions ({sessionsTotal})</h2>
|
|
1112
|
+
<button className="btn btn-ghost btn-sm" onClick={loadSessions} style={{ fontSize: 12 }}>🔄 Refresh</button>
|
|
1113
|
+
</div>
|
|
1114
|
+
|
|
1115
|
+
{sessionsLoading ? (
|
|
1116
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
1117
|
+
{[1, 2, 3].map(i => <div key={i} className="skeleton" style={{ height: 56 }} />)}
|
|
1118
|
+
</div>
|
|
1119
|
+
) : sessionsList.length === 0 ? (
|
|
1120
|
+
<div style={{ textAlign: "center", padding: 40, color: "var(--text-tertiary)" }}>
|
|
1121
|
+
<span style={{ fontSize: 32, display: "block", marginBottom: 8 }}>📝</span>
|
|
1122
|
+
No sessions ingested yet. Sessions appear here when the relay ships IDE conversation data.
|
|
1123
|
+
</div>
|
|
1124
|
+
) : (
|
|
1125
|
+
<table style={{ width: "100%", textAlign: "left", borderCollapse: "collapse" }}>
|
|
1126
|
+
<thead>
|
|
1127
|
+
<tr>
|
|
1128
|
+
<th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Title</th>
|
|
1129
|
+
<th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Date</th>
|
|
1130
|
+
<th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Source</th>
|
|
1131
|
+
<th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600 }}>Messages</th>
|
|
1132
|
+
<th style={{ padding: "10px 12px", borderBottom: "1px solid var(--border)", color: "var(--text-tertiary)", fontSize: 12, fontWeight: 600, width: 60 }}></th>
|
|
1133
|
+
</tr>
|
|
1134
|
+
</thead>
|
|
1135
|
+
<tbody>
|
|
1136
|
+
{sessionsList.map(s => {
|
|
1137
|
+
const sourceColors: Record<string, string> = {
|
|
1138
|
+
antigravity: "#8b5cf6",
|
|
1139
|
+
"claude-code": "#f59e0b",
|
|
1140
|
+
codex: "#10b981",
|
|
1141
|
+
vscode: "#3b82f6",
|
|
1142
|
+
cursor: "#06b6d4",
|
|
1143
|
+
"gemini-cli": "#ec4899",
|
|
1144
|
+
}
|
|
1145
|
+
const color = sourceColors[s.source || ""] || "var(--text-secondary)"
|
|
1146
|
+
return (
|
|
1147
|
+
<tr key={s.id} style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
|
|
1148
|
+
<td style={{ padding: "10px 12px", color: "#fff", fontSize: 13, maxWidth: 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
1149
|
+
{s.title || s.id}
|
|
1150
|
+
</td>
|
|
1151
|
+
<td style={{ padding: "10px 12px", color: "var(--text-secondary)", fontSize: 12, whiteSpace: "nowrap" }}>
|
|
1152
|
+
{s.date ? new Date(s.date).toLocaleString() : "-"}
|
|
1153
|
+
</td>
|
|
1154
|
+
<td style={{ padding: "10px 12px" }}>
|
|
1155
|
+
{s.source && (
|
|
1156
|
+
<span style={{
|
|
1157
|
+
fontSize: 10,
|
|
1158
|
+
fontWeight: 600,
|
|
1159
|
+
textTransform: "uppercase",
|
|
1160
|
+
padding: "2px 8px",
|
|
1161
|
+
borderRadius: 4,
|
|
1162
|
+
background: `${color}20`,
|
|
1163
|
+
color,
|
|
1164
|
+
border: `1px solid ${color}40`,
|
|
1165
|
+
}}>
|
|
1166
|
+
{s.source}
|
|
1167
|
+
</span>
|
|
1168
|
+
)}
|
|
1169
|
+
</td>
|
|
1170
|
+
<td style={{ padding: "10px 12px", color: "var(--text-secondary)", fontSize: 12 }}>
|
|
1171
|
+
{s.count}
|
|
1172
|
+
</td>
|
|
1173
|
+
<td style={{ padding: "10px 12px" }}>
|
|
1174
|
+
<button
|
|
1175
|
+
className="btn btn-ghost btn-sm"
|
|
1176
|
+
onClick={() => handleDeleteSession(s.id)}
|
|
1177
|
+
style={{ fontSize: 10, padding: "2px 6px", color: "#fca5a5", borderColor: "rgba(239,68,68,0.2)" }}
|
|
1178
|
+
>
|
|
1179
|
+
🗑️
|
|
1180
|
+
</button>
|
|
1181
|
+
</td>
|
|
1182
|
+
</tr>
|
|
1183
|
+
)
|
|
1184
|
+
})}
|
|
1185
|
+
</tbody>
|
|
1186
|
+
</table>
|
|
1187
|
+
)}
|
|
1188
|
+
</div>
|
|
1189
|
+
)}
|
|
1190
|
+
</div>
|
|
1191
|
+
)
|
|
1192
|
+
}
|