ltcai 4.6.0 → 4.7.0

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.
Files changed (37) hide show
  1. package/README.md +145 -194
  2. package/docs/CHANGELOG.md +139 -1
  3. package/docs/PRODUCT_DIRECTION_REVIEW.md +88 -0
  4. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +33 -19
  5. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +42 -0
  6. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +42 -0
  7. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +20 -18
  8. package/frontend/src/App.tsx +1098 -171
  9. package/frontend/src/api/client.ts +2 -0
  10. package/frontend/src/components/BrainConversation.tsx +10 -2
  11. package/frontend/src/components/LivingBrain.tsx +197 -106
  12. package/frontend/src/components/ProductFlow.tsx +210 -129
  13. package/frontend/src/styles.css +1946 -36
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/archive.py +86 -13
  16. package/lattice_brain/portability.py +82 -14
  17. package/lattice_brain/runtime/multi_agent.py +1 -1
  18. package/latticeai/__init__.py +1 -1
  19. package/latticeai/api/admin.py +30 -4
  20. package/latticeai/api/chat.py +25 -11
  21. package/latticeai/app_factory.py +8 -2
  22. package/latticeai/core/audit.py +3 -2
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/package.json +1 -1
  26. package/scripts/launch-pts-grok.sh +56 -0
  27. package/src-tauri/Cargo.lock +1 -1
  28. package/src-tauri/Cargo.toml +1 -1
  29. package/src-tauri/tauri.conf.json +1 -1
  30. package/static/app/asset-manifest.json +5 -5
  31. package/static/app/assets/index-DFmuiJ6t.css +2 -0
  32. package/static/app/assets/index-DwX3rNfA.js +16 -0
  33. package/static/app/assets/index-DwX3rNfA.js.map +1 -0
  34. package/static/app/index.html +2 -2
  35. package/static/app/assets/index-By-G-Kay.css +0 -2
  36. package/static/app/assets/index-CJx6WuQH.js +0 -336
  37. package/static/app/assets/index-CJx6WuQH.js.map +0 -1
@@ -1,220 +1,1147 @@
1
1
  import * as React from "react";
2
- import { useQuery } from "@tanstack/react-query";
3
- import { BrainCircuit, Command, Menu, Moon, Search, Sun, X } from "lucide-react";
4
- import { latticeApi } from "@/api/client";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import {
4
+ Activity,
5
+ Archive,
6
+ ArrowLeft,
7
+ ChevronDown,
8
+ DatabaseBackup,
9
+ Download,
10
+ Eye,
11
+ ImagePlus,
12
+ RotateCcw,
13
+ Search,
14
+ Send,
15
+ ServerCog,
16
+ ShieldCheck,
17
+ Users,
18
+ } from "lucide-react";
19
+ import { latticeApi, type ApiResult } from "@/api/client";
5
20
  import { Button } from "@/components/ui/button";
6
- import { Input } from "@/components/ui/input";
21
+ import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
7
22
  import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
8
23
  import { useAppStore } from "@/store/appStore";
9
- import { commandRoutes, go, parseHash, primaryRoutes, PrimaryRoute } from "@/routes";
10
- import { BrainPage } from "@/pages/Brain";
11
- import { CapturePage } from "@/pages/Capture";
12
- import { ActPage } from "@/pages/Act";
13
- import { LibraryPage } from "@/pages/Library";
14
- import { SystemPage } from "@/pages/System";
15
- import { cn } from "@/lib/utils";
16
-
17
- function useRoute() {
18
- const [route, setRoute] = React.useState(parseHash);
24
+ import { asArray } from "@/lib/utils";
25
+
26
+ type ApiRecord = Record<string, unknown>;
27
+ type BrainDepth = 1 | 2 | 3 | 4 | 5;
28
+
29
+ type Message = {
30
+ role: "user" | "assistant";
31
+ content: string;
32
+ };
33
+
34
+ type MemoryFragment = {
35
+ id: string;
36
+ title: string;
37
+ kind: string;
38
+ };
39
+
40
+ type KnowledgeConcept = {
41
+ id: string;
42
+ label: string;
43
+ type: string;
44
+ summary: string;
45
+ importance: number;
46
+ };
47
+
48
+ type RelationshipThread = {
49
+ id: string;
50
+ source: string;
51
+ target: string;
52
+ label: string;
53
+ weight: number;
54
+ };
55
+
56
+ type KnowledgeGraphModel = {
57
+ nodes: KnowledgeConcept[];
58
+ edges: RelationshipThread[];
59
+ };
60
+
61
+ const DEPTHS: Array<{ level: BrainDepth; label: string; state: BrainState }> = [
62
+ { level: 1, label: "Living Brain", state: "idle" },
63
+ { level: 2, label: "Memory Layer", state: "recalling" },
64
+ { level: 3, label: "Knowledge Layer", state: "synthesizing" },
65
+ { level: 4, label: "Relationship Layer", state: "planning" },
66
+ { level: 5, label: "Knowledge Graph", state: "synthesizing" },
67
+ ];
68
+
69
+ const STARTER_PROMPTS = [
70
+ "Remember this decision: ",
71
+ "What do I already know about ",
72
+ "Help me turn this project context into a plan: ",
73
+ ];
74
+
75
+ export default function App() {
76
+ const theme = useAppStore((state) => state.theme);
77
+ const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
78
+ const route = useHashRoute();
79
+ const { state: brainState, intensity, setBrain } = useBrainState();
80
+
81
+ React.useEffect(() => {
82
+ document.documentElement.dataset.theme = theme;
83
+ }, [theme]);
84
+
19
85
  React.useEffect(() => {
20
- const onHash = () => setRoute(parseHash());
21
- window.addEventListener("hashchange", onHash);
22
- return () => window.removeEventListener("hashchange", onHash);
86
+ const onKey = (event: KeyboardEvent) => {
87
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
88
+ event.preventDefault();
89
+ document.querySelector<HTMLTextAreaElement>(".brain-composer textarea")?.focus();
90
+ }
91
+ };
92
+ window.addEventListener("keydown", onKey);
93
+ return () => window.removeEventListener("keydown", onKey);
23
94
  }, []);
24
- return route;
25
- }
26
95
 
27
- function Page({ primary, tab }: { primary: PrimaryRoute; tab?: string }) {
28
- if (primary === "memory") return <BrainPage initialTab="memory" />;
29
- if (primary === "capture") return <CapturePage initialTab={tab} />;
30
- if (primary === "act") return <ActPage initialTab={tab} />;
31
- if (primary === "library") return <LibraryPage initialTab={tab} />;
32
- if (primary === "system") return <SystemPage initialTab={tab} />;
33
- return <BrainPage initialTab={tab} />;
34
- }
96
+ if (!flowComplete) {
97
+ return <ProductFlow onComplete={() => setFlowComplete(true)} />;
98
+ }
35
99
 
36
- function AmbientBrain() {
37
100
  return (
38
- <div className="ambient-brain" aria-hidden="true">
39
- <span className="signal-line signal-line-a" />
40
- <span className="signal-line signal-line-b" />
41
- <span className="signal-line signal-line-c" />
42
- <span className="signal-tile signal-tile-a" />
43
- <span className="signal-tile signal-tile-b" />
44
- <span className="signal-tile signal-tile-c" />
101
+ <div className="brain-space">
102
+ <div className="brain-field" />
103
+ {route.startsWith("/admin") ? (
104
+ <AdminConsole onBack={() => navigateHash("/brain")} />
105
+ ) : (
106
+ <BrainHome brainState={brainState} intensity={intensity} onBrainChange={setBrain} />
107
+ )}
45
108
  </div>
46
109
  );
47
110
  }
48
111
 
49
- function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) {
50
- const [query, setQuery] = React.useState("");
51
- const normalized = query.trim().toLowerCase();
52
- const matches = commandRoutes.filter((route) => (
53
- route.label.toLowerCase().includes(normalized) || route.key.includes(normalized)
54
- ));
112
+ function useHashRoute() {
113
+ const read = React.useCallback(() => {
114
+ const hash = window.location.hash.replace(/^#/, "");
115
+ return hash.startsWith("/") ? hash : "/brain";
116
+ }, []);
117
+ const [route, setRoute] = React.useState(read);
55
118
 
56
119
  React.useEffect(() => {
57
- if (!open) return;
58
- const onKey = (event: KeyboardEvent) => {
59
- if (event.key === "Escape") onClose();
120
+ const onHashChange = () => setRoute(read());
121
+ window.addEventListener("hashchange", onHashChange);
122
+ return () => window.removeEventListener("hashchange", onHashChange);
123
+ }, [read]);
124
+
125
+ return route;
126
+ }
127
+
128
+ function navigateHash(route: string) {
129
+ window.location.hash = route;
130
+ }
131
+
132
+ function useBrainState() {
133
+ const [state, setState] = React.useState<BrainState>("idle");
134
+ const [intensity, setIntensity] = React.useState(0.58);
135
+
136
+ const setBrain = React.useCallback((next: BrainState, nextIntensity?: number) => {
137
+ setState(next);
138
+ if (nextIntensity !== undefined) setIntensity(clamp(nextIntensity, 0.38, 1));
139
+ }, []);
140
+
141
+ return { state, intensity, setBrain };
142
+ }
143
+
144
+ function BrainHome({
145
+ brainState,
146
+ intensity,
147
+ onBrainChange,
148
+ }: {
149
+ brainState: BrainState;
150
+ intensity: number;
151
+ onBrainChange: (state: BrainState, intensity?: number) => void;
152
+ }) {
153
+ const qc = useQueryClient();
154
+ const [messages, setMessages] = React.useState<Message[]>([]);
155
+ const [draft, setDraft] = React.useState("");
156
+ const [imageData, setImageData] = React.useState<string | null>(null);
157
+ const [streaming, setStreaming] = React.useState(false);
158
+ const [conversationId, setConversationId] = React.useState<string | null>(null);
159
+ const [explorationDepth, setExplorationDepth] = React.useState<BrainDepth>(1);
160
+ const [graphSearch, setGraphSearch] = React.useState("");
161
+ const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
162
+ const streamRef = React.useRef<HTMLDivElement>(null);
163
+ const recallTimerRef = React.useRef<number | null>(null);
164
+
165
+ const memoriesQ = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
166
+ const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
167
+ const graphQ = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
168
+ const modelsQ = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
169
+
170
+ const memoryFragments = React.useMemo(
171
+ () => buildMemoryFragments(memoriesQ.data?.data, historyQ.data?.data),
172
+ [memoriesQ.data, historyQ.data],
173
+ );
174
+ const graphModel = React.useMemo(() => parseKnowledgeGraph(graphQ.data?.data), [graphQ.data]);
175
+ const knowledgeConcepts = React.useMemo(
176
+ () => graphModel.nodes.slice(0, 10),
177
+ [graphModel.nodes],
178
+ );
179
+ const relationshipThreads = React.useMemo(
180
+ () => graphModel.edges.slice(0, 10),
181
+ [graphModel.edges],
182
+ );
183
+ const modelName = React.useMemo(() => currentModelName(modelsQ.data?.data), [modelsQ.data]);
184
+ const currentDepth = DEPTHS[explorationDepth - 1];
185
+
186
+ React.useEffect(() => {
187
+ if (streaming) onBrainChange("thinking", 0.94);
188
+ else if (draft.trim().length > 4) onBrainChange("listening", 0.76);
189
+ else onBrainChange(currentDepth.state, explorationDepth === 1 ? 0.58 : 0.66 + explorationDepth * 0.06);
190
+ }, [streaming, draft, currentDepth.state, explorationDepth, onBrainChange]);
191
+
192
+ React.useEffect(() => {
193
+ const stream = streamRef.current;
194
+ if (stream) stream.scrollTop = stream.scrollHeight;
195
+ }, [messages]);
196
+
197
+ React.useEffect(() => {
198
+ return () => {
199
+ if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
60
200
  };
61
- window.addEventListener("keydown", onKey);
62
- return () => window.removeEventListener("keydown", onKey);
63
- }, [open, onClose]);
201
+ }, []);
202
+
203
+ async function send() {
204
+ const text = draft.trim();
205
+ if (!text || streaming) return;
206
+ const activeConversationId = conversationId || `brain-${Date.now()}`;
207
+ if (!conversationId) setConversationId(activeConversationId);
208
+
209
+ setMessages((items) => [...items, { role: "user", content: text }, { role: "assistant", content: "" }]);
210
+ setDraft("");
211
+ setImageData(null);
212
+ setStreaming(true);
213
+ onBrainChange("thinking", 0.96);
214
+
215
+ try {
216
+ const result = await latticeApi.streamChat(
217
+ { message: text, conversation_id: activeConversationId, image_data: imageData || undefined },
218
+ {
219
+ onChunk: (_delta, fullText) => {
220
+ setMessages((items) => {
221
+ const next = [...items];
222
+ next[next.length - 1] = { role: "assistant", content: fullText };
223
+ return next;
224
+ });
225
+ },
226
+ onTrace: (trace) => {
227
+ if (!trace) return;
228
+ onBrainChange("recalling", 0.9);
229
+ triggerBrainRecall();
230
+ if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
231
+ recallTimerRef.current = window.setTimeout(() => onBrainChange("thinking", 0.9), 900);
232
+ },
233
+ },
234
+ );
235
+ if (result.error) {
236
+ setMessages((items) => {
237
+ const next = [...items];
238
+ next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
239
+ return next;
240
+ });
241
+ }
242
+ } finally {
243
+ setStreaming(false);
244
+ void qc.invalidateQueries({ queryKey: ["chatHistory"] });
245
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
246
+ void qc.invalidateQueries({ queryKey: ["graph"] });
247
+ }
248
+ }
249
+
250
+ function deepen() {
251
+ setExplorationDepth((depth) => {
252
+ const next = Math.min(5, depth + 1) as BrainDepth;
253
+ const nextDepth = DEPTHS[next - 1];
254
+ onBrainChange(nextDepth.state, 0.66 + next * 0.06);
255
+ if (next >= 2) triggerBrainRecall();
256
+ return next;
257
+ });
258
+ }
259
+
260
+ function surface() {
261
+ setExplorationDepth(1);
262
+ setSelectedGraphId(null);
263
+ setGraphSearch("");
264
+ onBrainChange("idle", 0.58);
265
+ }
266
+
267
+ function recallMemory(fragment: MemoryFragment) {
268
+ triggerBrainRecall();
269
+ setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
270
+ setMessages((items) => [
271
+ ...items,
272
+ { role: "assistant", content: `I am recalling ${fragment.kind.toLowerCase()}: ${fragment.title}` },
273
+ ]);
274
+ }
64
275
 
65
- if (!open) return null;
66
276
  return (
67
- <div className="command-scrim" role="dialog" aria-modal="true" aria-label="Lattice command palette">
68
- <div className="command-panel">
69
- <div className="command-search">
70
- <Search className="h-4 w-4 text-muted-foreground" />
71
- <Input value={query} onChange={(event) => setQuery(event.target.value)} autoFocus placeholder="Jump to anything in Lattice" />
72
- <Button variant="ghost" size="icon" onClick={onClose} aria-label="Close command palette"><X className="h-4 w-4" /></Button>
277
+ <main className="brain-home" aria-label="Lattice Brain">
278
+ <section className="brain-presence" aria-label="Brain exploration">
279
+ <div className="brain-exploration" data-depth={explorationDepth}>
280
+ <LivingBrain
281
+ state={brainState}
282
+ intensity={intensity + explorationDepth * 0.035}
283
+ size="large"
284
+ depth={explorationDepth}
285
+ showLabel={false}
286
+ onInteract={deepen}
287
+ />
288
+
289
+ <div className="brain-depth-badge" aria-live="polite">
290
+ <span>Level {explorationDepth}</span>
291
+ <strong>{currentDepth.label}</strong>
292
+ </div>
293
+
294
+ <div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
295
+ <DepthEmergence
296
+ depth={explorationDepth}
297
+ memories={memoryFragments}
298
+ concepts={knowledgeConcepts}
299
+ relationships={relationshipThreads}
300
+ graphModel={graphModel}
301
+ graphSearch={graphSearch}
302
+ selectedGraphId={selectedGraphId}
303
+ onGraphSearch={setGraphSearch}
304
+ onSelectGraphNode={setSelectedGraphId}
305
+ onRecallMemory={recallMemory}
306
+ />
307
+ </div>
308
+
309
+ {explorationDepth > 1 ? (
310
+ <button className="brain-surface-control" type="button" onClick={surface}>
311
+ Surface
312
+ </button>
313
+ ) : null}
73
314
  </div>
74
- <div className="command-list soft-scrollbar">
75
- {matches.map((route) => {
76
- const Icon = route.icon;
77
- return (
78
- <button
79
- key={route.key}
80
- onClick={() => {
81
- go(route.key);
82
- onClose();
315
+ </section>
316
+
317
+ <section className="brain-conversation" aria-label="Conversation">
318
+ <div className="brain-conversation-header">
319
+ <div>
320
+ <h1>Lattice Brain</h1>
321
+ <span>{currentDepth.label}</span>
322
+ </div>
323
+ <div className="brain-ownership-strip" aria-label="Brain ownership guarantees">
324
+ <span>Local-first</span>
325
+ <span>Portable</span>
326
+ <span>Private</span>
327
+ </div>
328
+ <div>{modelName}</div>
329
+ <button className="brain-admin-link" type="button" onClick={() => navigateHash("/admin")}>
330
+ <ShieldCheck className="h-3.5 w-3.5" />
331
+ Admin
332
+ </button>
333
+ </div>
334
+
335
+ <div ref={streamRef} className="brain-stream">
336
+ {messages.length === 0 ? (
337
+ <div className="mind-empty">
338
+ <div className="mind-empty-kicker">Your durable context</div>
339
+ <div className="mind-empty-title">Start with what should not be forgotten.</div>
340
+ <p>
341
+ Lattice keeps your documents, conversations, projects, and decisions available while models can change around them.
342
+ </p>
343
+ <div className="mind-empty-prompts" aria-label="Starter prompts">
344
+ {STARTER_PROMPTS.map((prompt) => (
345
+ <button key={prompt} type="button" onClick={() => setDraft(prompt)}>
346
+ {prompt}
347
+ </button>
348
+ ))}
349
+ </div>
350
+ </div>
351
+ ) : (
352
+ messages.map((message, index) => (
353
+ <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
354
+ <div className="brain-message-bubble">{message.content}</div>
355
+ </div>
356
+ ))
357
+ )}
358
+ </div>
359
+
360
+ <BrainCarePanel />
361
+
362
+ <div className="brain-composer">
363
+ <textarea
364
+ value={draft}
365
+ onChange={(event) => setDraft(event.target.value)}
366
+ onKeyDown={(event) => {
367
+ if (event.key === "Enter" && !event.shiftKey) {
368
+ event.preventDefault();
369
+ void send();
370
+ }
371
+ }}
372
+ placeholder="Talk to your Brain..."
373
+ />
374
+ <div className="brain-composer-actions">
375
+ <label className="brain-image-input">
376
+ <ImagePlus className="h-3.5 w-3.5" />
377
+ <span>Image</span>
378
+ <input
379
+ type="file"
380
+ accept="image/*"
381
+ className="sr-only"
382
+ onChange={async (event) => {
383
+ const file = event.target.files?.[0];
384
+ if (file) setImageData(await fileToDataUrl(file));
83
385
  }}
84
- className="command-row"
85
- >
86
- <span className="command-icon"><Icon className="h-4 w-4" /></span>
87
- <span>
88
- <span className="block text-sm font-semibold">{route.label}</span>
89
- <span className="block text-xs text-muted-foreground">Open {route.key.replace(/[-/]/g, " ")}</span>
90
- </span>
91
- </button>
92
- );
93
- })}
386
+ />
387
+ </label>
388
+ {imageData ? <span className="brain-quiet-success">Image attached</span> : null}
389
+ <Button onClick={() => void send()} disabled={!draft.trim() || streaming} className="rounded-full px-5">
390
+ <Send className="h-4 w-4" /> Send
391
+ </Button>
392
+ </div>
94
393
  </div>
95
- </div>
394
+ </section>
395
+ </main>
396
+ );
397
+ }
398
+
399
+ function AdminConsole({ onBack }: { onBack: () => void }) {
400
+ const qc = useQueryClient();
401
+ const summaryQ = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
402
+ const statsQ = useQuery({ queryKey: ["adminStats"], queryFn: latticeApi.adminStats });
403
+ const usersQ = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
404
+ const auditQ = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
405
+ const securityQ = useQuery({ queryKey: ["adminSecurity"], queryFn: latticeApi.adminSecurity });
406
+ const securityEventsQ = useQuery({ queryKey: ["adminSecurityEvents"], queryFn: () => latticeApi.adminSecurityEvents(50) });
407
+ const policiesQ = useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies });
408
+ const indexQ = useQuery({ queryKey: ["indexStatus"], queryFn: latticeApi.indexStatus });
409
+ const rebuildIndex = useMutation({
410
+ mutationFn: latticeApi.rebuildIndex,
411
+ onSuccess: () => void qc.invalidateQueries({ queryKey: ["indexStatus"] }),
412
+ });
413
+
414
+ const users = asArray(usersQ.data?.data);
415
+ const auditEvents = asArray((auditQ.data?.data as ApiRecord | undefined)?.recent_events);
416
+ const securityEvents = asArray((securityEventsQ.data?.data as ApiRecord | undefined)?.events);
417
+ const policies = asArray((policiesQ.data?.data as ApiRecord | undefined)?.policies);
418
+
419
+ return (
420
+ <main className="admin-console" aria-label="Lattice Admin">
421
+ <header className="admin-console-header">
422
+ <button className="admin-back-button" type="button" onClick={onBack}>
423
+ <ArrowLeft className="h-4 w-4" />
424
+ Brain
425
+ </button>
426
+ <div>
427
+ <span>Separate admin workspace</span>
428
+ <h1>Admin Console</h1>
429
+ <p>Users, logs, security, and Brain health stay out of the normal user experience.</p>
430
+ </div>
431
+ </header>
432
+
433
+ <section className="admin-metrics" aria-label="Admin overview">
434
+ <AdminMetric icon={<Users className="h-4 w-4" />} label="Users" value={String(users.length)} detail={sourceLabel(usersQ.data)} />
435
+ <AdminMetric
436
+ icon={<Activity className="h-4 w-4" />}
437
+ label="Recent logs"
438
+ value={String(auditEvents.length + securityEvents.length)}
439
+ detail={sourceLabel(auditQ.data)}
440
+ />
441
+ <AdminMetric
442
+ icon={<ShieldCheck className="h-4 w-4" />}
443
+ label="Security"
444
+ value={adminStatusLabel(securityQ.data?.data, "status") || (securityQ.data?.ok ? "Ready" : "Unavailable")}
445
+ detail={sourceLabel(securityQ.data)}
446
+ />
447
+ <AdminMetric
448
+ icon={<ServerCog className="h-4 w-4" />}
449
+ label="Brain index"
450
+ value={adminStatusLabel(indexQ.data?.data, "status") || (indexQ.data?.ok ? "Indexed" : "Unknown")}
451
+ detail={indexDetail(indexQ.data?.data)}
452
+ />
453
+ </section>
454
+
455
+ <section className="admin-grid">
456
+ <AdminPanel title="User Directory" eyebrow="People">
457
+ <AdminList
458
+ items={users.slice(0, 8)}
459
+ empty="No users reported by the admin API."
460
+ render={(item) => {
461
+ const user = item as ApiRecord;
462
+ return (
463
+ <>
464
+ <strong>{stringValue(user.name || user.email || user.id, "Local user")}</strong>
465
+ <span>{stringValue(user.role || user.status || user.workspace_id, "member")}</span>
466
+ </>
467
+ );
468
+ }}
469
+ />
470
+ </AdminPanel>
471
+
472
+ <AdminPanel title="Activity Logs" eyebrow="Audit">
473
+ <AdminList
474
+ items={auditEvents.slice(0, 8)}
475
+ empty="No recent audit events."
476
+ render={(item) => renderLogRow(item as ApiRecord)}
477
+ />
478
+ </AdminPanel>
479
+
480
+ <AdminPanel title="Security Events" eyebrow="Protection">
481
+ <AdminList
482
+ items={securityEvents.slice(0, 8)}
483
+ empty="No security events reported."
484
+ render={(item) => renderLogRow(item as ApiRecord)}
485
+ />
486
+ </AdminPanel>
487
+
488
+ <AdminPanel title="Brain Operations" eyebrow="Maintenance">
489
+ <div className="admin-operation">
490
+ <div>
491
+ <strong>{indexDetail(indexQ.data?.data)}</strong>
492
+ <span>{summaryText(summaryQ.data?.data) || summaryText(statsQ.data?.data) || "Local Brain services are separated from user chat."}</span>
493
+ </div>
494
+ <Button variant="outline" size="sm" disabled={rebuildIndex.isPending} onClick={() => rebuildIndex.mutate()}>
495
+ <RotateCcw className="h-3.5 w-3.5" />
496
+ {rebuildIndex.isPending ? "Rebuilding" : "Rebuild index"}
497
+ </Button>
498
+ </div>
499
+ <div className="admin-policy-strip">
500
+ {policies.slice(0, 5).map((item, index) => {
501
+ const policy = item as ApiRecord;
502
+ return <span key={`${stringValue(policy.id || policy.name, "policy")}-${index}`}>{stringValue(policy.name || policy.id, "Policy")}</span>;
503
+ })}
504
+ {!policies.length ? <span>Policy API quiet</span> : null}
505
+ </div>
506
+ </AdminPanel>
507
+ </section>
508
+ </main>
509
+ );
510
+ }
511
+
512
+ function AdminMetric({ icon, label, value, detail }: { icon: React.ReactNode; label: string; value: string; detail: string }) {
513
+ return (
514
+ <div className="admin-metric">
515
+ <div>{icon}</div>
516
+ <span>{label}</span>
517
+ <strong>{value}</strong>
518
+ <small>{detail}</small>
96
519
  </div>
97
520
  );
98
521
  }
99
522
 
100
- function PrimaryDock({ active, onNavigate }: { active: PrimaryRoute; onNavigate?: () => void }) {
523
+ function AdminPanel({ eyebrow, title, children }: { eyebrow: string; title: string; children: React.ReactNode }) {
524
+ return (
525
+ <section className="admin-panel">
526
+ <div className="admin-panel-head">
527
+ <span>{eyebrow}</span>
528
+ <h2>{title}</h2>
529
+ </div>
530
+ {children}
531
+ </section>
532
+ );
533
+ }
534
+
535
+ function AdminList({ items, empty, render }: { items: unknown[]; empty: string; render: (item: unknown) => React.ReactNode }) {
536
+ if (!items.length) return <div className="admin-empty">{empty}</div>;
537
+ return <div className="admin-list">{items.map((item, index) => <div key={index} className="admin-list-row">{render(item)}</div>)}</div>;
538
+ }
539
+
540
+ function BrainCarePanel() {
541
+ const qc = useQueryClient();
542
+ const [expanded, setExpanded] = React.useState(false);
543
+ const [archivePath, setArchivePath] = React.useState("");
544
+ const [passphrase, setPassphrase] = React.useState("");
545
+ const [latestResult, setLatestResult] = React.useState<ApiResult | null>(null);
546
+ const portabilityQ = useQuery({ queryKey: ["portability"], queryFn: latticeApi.graphPortability });
547
+ const backupHealthQ = useQuery({ queryKey: ["backupHealth"], queryFn: latticeApi.backupHealth });
548
+ const rememberResult = React.useCallback((result: ApiResult) => setLatestResult(result), []);
549
+
550
+ const exportGraph = useCareMutation(() => latticeApi.graphExport(), undefined, rememberResult);
551
+ const backupGraph = useCareMutation(() => latticeApi.graphBackup(), () => {
552
+ void qc.invalidateQueries({ queryKey: ["backupHealth"] });
553
+ void qc.invalidateQueries({ queryKey: ["portability"] });
554
+ }, rememberResult);
555
+ const archiveBrain = useCareMutation(
556
+ () => latticeApi.brainArchive({ path: archivePath.trim() || null, passphrase }),
557
+ () => void qc.invalidateQueries({ queryKey: ["backupHealth"] }),
558
+ rememberResult,
559
+ );
560
+ const inspectArchive = useCareMutation(() => latticeApi.brainArchiveInspect({
561
+ path: archivePath.trim(),
562
+ passphrase: passphrase || null,
563
+ }), undefined, rememberResult);
564
+ const restorePreview = useCareMutation(() => latticeApi.brainArchiveRestore({
565
+ path: archivePath.trim(),
566
+ passphrase,
567
+ dry_run: true,
568
+ confirm: false,
569
+ }), undefined, rememberResult);
570
+
571
+ const portableFormat = portabilityLabel(portabilityQ.data?.data);
572
+ const backupStatus = backupHealthLabel(backupHealthQ.data?.data);
573
+
574
+ return (
575
+ <section className={`brain-care-panel ${expanded ? "is-expanded" : "is-collapsed"}`} aria-label="Care for my Brain">
576
+ <button
577
+ className="brain-care-summary"
578
+ type="button"
579
+ aria-expanded={expanded}
580
+ aria-controls="brain-care-details"
581
+ onClick={() => setExpanded((value) => !value)}
582
+ >
583
+ <span className="brain-care-summary-main">
584
+ <span><ShieldCheck className="h-3.5 w-3.5" /> Care for my Brain</span>
585
+ <strong>Own it locally. Keep it portable.</strong>
586
+ </span>
587
+ <div className="brain-care-proof" aria-label="Ownership model">
588
+ <span>Private</span>
589
+ <span>{portableFormat}</span>
590
+ <span>{backupStatus}</span>
591
+ </div>
592
+ <ChevronDown className="brain-care-toggle h-4 w-4" aria-hidden="true" />
593
+ </button>
594
+
595
+ {expanded ? (
596
+ <div id="brain-care-details" className="brain-care-details">
597
+ <div className="brain-care-actions">
598
+ <CareButton
599
+ icon={<Download className="h-3.5 w-3.5" />}
600
+ label="Export"
601
+ detail="Take it with you"
602
+ pending={exportGraph.isPending}
603
+ onClick={() => exportGraph.mutate()}
604
+ />
605
+ <CareButton
606
+ icon={<DatabaseBackup className="h-3.5 w-3.5" />}
607
+ label="Backup"
608
+ detail="Save a copy"
609
+ pending={backupGraph.isPending}
610
+ onClick={() => backupGraph.mutate()}
611
+ />
612
+ <CareButton
613
+ icon={<Archive className="h-3.5 w-3.5" />}
614
+ label="Archive"
615
+ detail="Encrypted Brain"
616
+ pending={archiveBrain.isPending}
617
+ disabled={!passphrase.trim()}
618
+ onClick={() => archiveBrain.mutate()}
619
+ />
620
+ </div>
621
+
622
+ <div className="brain-care-archive">
623
+ <input
624
+ value={archivePath}
625
+ onChange={(event) => setArchivePath(event.target.value)}
626
+ placeholder="Paste an archive path to inspect or preview"
627
+ aria-label="Brain archive path"
628
+ />
629
+ <input
630
+ type="password"
631
+ value={passphrase}
632
+ onChange={(event) => setPassphrase(event.target.value)}
633
+ placeholder="Archive passphrase"
634
+ aria-label="Brain archive passphrase"
635
+ />
636
+ <div className="brain-care-archive-actions">
637
+ <Button
638
+ variant="outline"
639
+ size="sm"
640
+ disabled={!archivePath.trim() || inspectArchive.isPending}
641
+ onClick={() => inspectArchive.mutate()}
642
+ >
643
+ <Eye className="h-3.5 w-3.5" /> Inspect
644
+ </Button>
645
+ <Button
646
+ variant="outline"
647
+ size="sm"
648
+ disabled={!archivePath.trim() || !passphrase.trim() || restorePreview.isPending}
649
+ onClick={() => restorePreview.mutate()}
650
+ >
651
+ <RotateCcw className="h-3.5 w-3.5" /> Restore preview
652
+ </Button>
653
+ </div>
654
+ </div>
655
+
656
+ {latestResult ? (
657
+ <div className={`brain-care-result ${latestResult.ok ? "is-ok" : "is-error"}`} role="status">
658
+ {summarizeCareResult(latestResult)}
659
+ </div>
660
+ ) : (
661
+ <p className="brain-care-note">
662
+ Restore preview checks an archive without changing your Brain. Confirmed restore stays in Settings.
663
+ </p>
664
+ )}
665
+ </div>
666
+ ) : null}
667
+ </section>
668
+ );
669
+ }
670
+
671
+ function useCareMutation<T extends ApiResult>(
672
+ mutationFn: () => Promise<T>,
673
+ onSuccess?: () => void,
674
+ onResult?: (result: T) => void,
675
+ ) {
676
+ return useMutation({
677
+ mutationFn,
678
+ onSuccess: (result) => {
679
+ onResult?.(result);
680
+ onSuccess?.();
681
+ },
682
+ });
683
+ }
684
+
685
+ function CareButton({
686
+ icon,
687
+ label,
688
+ detail,
689
+ pending,
690
+ disabled,
691
+ onClick,
692
+ }: {
693
+ icon: React.ReactNode;
694
+ label: string;
695
+ detail: string;
696
+ pending?: boolean;
697
+ disabled?: boolean;
698
+ onClick: () => void;
699
+ }) {
700
+ return (
701
+ <button className="brain-care-button" type="button" disabled={disabled || pending} onClick={onClick}>
702
+ {icon}
703
+ <span>
704
+ <strong>{pending ? "Working" : label}</strong>
705
+ <small>{detail}</small>
706
+ </span>
707
+ </button>
708
+ );
709
+ }
710
+
711
+ function DepthEmergence({
712
+ depth,
713
+ memories,
714
+ concepts,
715
+ relationships,
716
+ graphModel,
717
+ graphSearch,
718
+ selectedGraphId,
719
+ onGraphSearch,
720
+ onSelectGraphNode,
721
+ onRecallMemory,
722
+ }: {
723
+ depth: BrainDepth;
724
+ memories: MemoryFragment[];
725
+ concepts: KnowledgeConcept[];
726
+ relationships: RelationshipThread[];
727
+ graphModel: KnowledgeGraphModel;
728
+ graphSearch: string;
729
+ selectedGraphId: string | null;
730
+ onGraphSearch: (value: string) => void;
731
+ onSelectGraphNode: (id: string | null) => void;
732
+ onRecallMemory: (fragment: MemoryFragment) => void;
733
+ }) {
734
+ if (depth === 1) return null;
735
+
736
+ return (
737
+ <>
738
+ {depth >= 2 ? (
739
+ <MemoryLayer memories={memories} depth={depth} onRecallMemory={onRecallMemory} />
740
+ ) : null}
741
+ {depth >= 3 && depth < 5 ? (
742
+ <KnowledgeLayer concepts={concepts} depth={depth} />
743
+ ) : null}
744
+ {depth >= 4 && depth < 5 ? (
745
+ <RelationshipLayer concepts={concepts} relationships={relationships} />
746
+ ) : null}
747
+ {depth >= 5 ? (
748
+ <EmergentKnowledgeGraph
749
+ model={graphModel}
750
+ search={graphSearch}
751
+ selectedId={selectedGraphId}
752
+ onSearch={onGraphSearch}
753
+ onSelect={onSelectGraphNode}
754
+ />
755
+ ) : null}
756
+ </>
757
+ );
758
+ }
759
+
760
+ function MemoryLayer({
761
+ memories,
762
+ depth,
763
+ onRecallMemory,
764
+ }: {
765
+ memories: MemoryFragment[];
766
+ depth: BrainDepth;
767
+ onRecallMemory: (fragment: MemoryFragment) => void;
768
+ }) {
769
+ const visible = memories.slice(0, depth >= 3 ? 8 : 6);
770
+ if (!visible.length) return <div className="memory-fragment is-empty">Memory is quiet</div>;
771
+
101
772
  return (
102
- <nav className="primary-dock" aria-label="Primary navigation">
103
- {primaryRoutes.map((item) => {
104
- const Icon = item.icon;
105
- const selected = active === item.id;
773
+ <>
774
+ {visible.map((memory, index) => {
775
+ const point = polarPoint(index, visible.length, depth >= 3 ? 39 : 31, depth >= 3 ? 24 : 18, -112);
106
776
  return (
107
777
  <button
108
- key={item.id}
109
- className={cn("dock-button", selected && "is-active")}
110
- onClick={() => {
111
- go(item.id);
112
- onNavigate?.();
113
- }}
114
- aria-current={selected ? "page" : undefined}
778
+ key={memory.id}
779
+ type="button"
780
+ className="memory-fragment"
781
+ style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 55}ms` })}
782
+ onClick={() => onRecallMemory(memory)}
115
783
  >
116
- <Icon className="h-4 w-4" />
117
- <span>{item.label}</span>
784
+ <span>{memory.kind}</span>
785
+ <strong>{memory.title}</strong>
118
786
  </button>
119
787
  );
120
788
  })}
121
- </nav>
789
+ </>
122
790
  );
123
791
  }
124
792
 
125
- export default function App() {
126
- const route = useRoute();
127
- const { theme, setTheme } = useAppStore();
128
- const [drawer, setDrawer] = React.useState(false);
129
- const [palette, setPalette] = React.useState(false);
130
- const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
131
- const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health, enabled: flowComplete });
132
- const desktop = useQuery({
133
- queryKey: ["desktopBackendStatus"],
134
- queryFn: latticeApi.desktopBackendStatus,
135
- enabled: flowComplete && Boolean(window.__TAURI_INTERNALS__),
136
- refetchInterval: 5000,
137
- });
793
+ function KnowledgeLayer({ concepts, depth }: { concepts: KnowledgeConcept[]; depth: BrainDepth }) {
794
+ const visible = concepts.slice(0, depth >= 4 ? 10 : 7);
795
+ if (!visible.length) return <div className="concept-signal is-empty">Knowledge is forming</div>;
138
796
 
139
- React.useEffect(() => {
140
- document.documentElement.dataset.theme = theme;
141
- }, [theme]);
797
+ return (
798
+ <>
799
+ {visible.map((concept, index) => {
800
+ const point = polarPoint(index, visible.length, 24, 15, -70);
801
+ return (
802
+ <button
803
+ key={concept.id}
804
+ type="button"
805
+ className="concept-signal"
806
+ style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 45}ms` })}
807
+ title={concept.summary || concept.type}
808
+ >
809
+ <span>{concept.type}</span>
810
+ {concept.label}
811
+ </button>
812
+ );
813
+ })}
814
+ </>
815
+ );
816
+ }
142
817
 
143
- React.useEffect(() => {
144
- const onKey = (event: KeyboardEvent) => {
145
- if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
146
- event.preventDefault();
147
- setPalette(true);
148
- }
149
- };
150
- window.addEventListener("keydown", onKey);
151
- return () => window.removeEventListener("keydown", onKey);
152
- }, []);
818
+ function RelationshipLayer({
819
+ concepts,
820
+ relationships,
821
+ }: {
822
+ concepts: KnowledgeConcept[];
823
+ relationships: RelationshipThread[];
824
+ }) {
825
+ const visibleConcepts = concepts.slice(0, 10);
826
+ const layout = layoutGraphNodes(visibleConcepts, 30, 20);
827
+ const positionById = new Map(layout.map((item) => [item.node.id, item]));
828
+ const visibleRelationships = relationships
829
+ .map((relationship, index) => {
830
+ const source = positionById.get(relationship.source) || layout[index % Math.max(layout.length, 1)];
831
+ const target = positionById.get(relationship.target) || layout[(index + 3) % Math.max(layout.length, 1)];
832
+ return source && target && source.node.id !== target.node.id ? { relationship, source, target } : null;
833
+ })
834
+ .filter(Boolean)
835
+ .slice(0, 8) as Array<{
836
+ relationship: RelationshipThread;
837
+ source: ReturnType<typeof layoutGraphNodes>[number];
838
+ target: ReturnType<typeof layoutGraphNodes>[number];
839
+ }>;
153
840
 
154
- if (!flowComplete) {
155
- return <ProductFlow onComplete={() => {
156
- setFlowComplete(true);
157
- go("brain");
158
- }} />;
159
- }
841
+ if (!visibleRelationships.length) return null;
160
842
 
161
- const healthData = (health.data?.data || {}) as Record<string, unknown>;
162
- const desktopData = (desktop.data?.data || {}) as Record<string, unknown>;
163
- const backendReady = Boolean(health.data?.ok);
164
- const desktopReady = !window.__TAURI_INTERNALS__ || Boolean(desktopData.running);
843
+ return (
844
+ <svg className="relationship-weave" viewBox="0 0 100 100" aria-hidden>
845
+ {visibleRelationships.map(({ relationship, source, target }, index) => (
846
+ <line
847
+ key={`${relationship.id}-${index}`}
848
+ x1={source.x}
849
+ y1={source.y}
850
+ x2={target.x}
851
+ y2={target.y}
852
+ style={{ animationDelay: `${index * 80}ms` }}
853
+ />
854
+ ))}
855
+ </svg>
856
+ );
857
+ }
858
+
859
+ function EmergentKnowledgeGraph({
860
+ model,
861
+ search,
862
+ selectedId,
863
+ onSearch,
864
+ onSelect,
865
+ }: {
866
+ model: KnowledgeGraphModel;
867
+ search: string;
868
+ selectedId: string | null;
869
+ onSearch: (value: string) => void;
870
+ onSelect: (id: string | null) => void;
871
+ }) {
872
+ const query = search.trim().toLowerCase();
873
+ const visibleNodes = React.useMemo(() => {
874
+ const filtered = model.nodes.filter((node) => {
875
+ if (!query) return true;
876
+ return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
877
+ });
878
+ return filtered.slice(0, 18);
879
+ }, [model.nodes, query]);
880
+ const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
881
+ const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
882
+ const visibleEdges = React.useMemo(
883
+ () => model.edges.filter((edge) => positionById.has(edge.source) && positionById.has(edge.target)).slice(0, 36),
884
+ [model.edges, positionById],
885
+ );
886
+ const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
165
887
 
166
888
  return (
167
- <div className="app-backdrop min-h-screen text-foreground">
168
- <AmbientBrain />
169
- <CommandPalette open={palette} onClose={() => setPalette(false)} />
170
-
171
- <header className="app-chrome">
172
- <div className="brand-lockup">
173
- <button className="mobile-menu" onClick={() => setDrawer(true)} aria-label="Open navigation"><Menu className="h-5 w-5" /></button>
174
- <button className="brand-mark" onClick={() => go("brain")} aria-label="Open Lattice Brain">
175
- <BrainCircuit className="h-5 w-5" />
176
- </button>
177
- <div className="brand-copy">
178
- <div className="brand-name">Lattice</div>
179
- <div className="brand-subtitle">Living Brain</div>
180
- </div>
889
+ <section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label="Knowledge Graph">
890
+ <div className="brain-graph-head">
891
+ <div>
892
+ <span>Level 5</span>
893
+ <strong>Knowledge Graph</strong>
181
894
  </div>
895
+ <label className="brain-graph-search">
896
+ <Search className="h-3.5 w-3.5" />
897
+ <input
898
+ value={search}
899
+ onChange={(event) => onSearch(event.target.value)}
900
+ placeholder="Search"
901
+ aria-label="Search knowledge graph"
902
+ />
903
+ </label>
904
+ </div>
182
905
 
183
- <div className="desktop-dock">
184
- <PrimaryDock active={route.primary} />
906
+ {visibleNodes.length ? (
907
+ <div className="brain-graph-canvas">
908
+ <svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
909
+ {visibleEdges.map((edge, index) => {
910
+ const source = positionById.get(edge.source);
911
+ const target = positionById.get(edge.target);
912
+ if (!source || !target) return null;
913
+ return (
914
+ <line
915
+ key={`${edge.id}-${index}`}
916
+ x1={source.x}
917
+ y1={source.y}
918
+ x2={target.x}
919
+ y2={target.y}
920
+ style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
921
+ />
922
+ );
923
+ })}
924
+ </svg>
925
+ {layout.map(({ node, x, y }, index) => (
926
+ <button
927
+ key={node.id}
928
+ type="button"
929
+ className={`graph-node ${selected?.id === node.id ? "is-selected" : ""}`}
930
+ style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
931
+ onClick={() => onSelect(node.id)}
932
+ >
933
+ <span>{node.type}</span>
934
+ {node.label}
935
+ </button>
936
+ ))}
185
937
  </div>
938
+ ) : (
939
+ <div className="brain-graph-empty">No matching knowledge yet</div>
940
+ )}
186
941
 
187
- <div className="chrome-actions">
188
- <button className="status-chip" onClick={() => go("settings")}>
189
- <span className={cn("status-light", backendReady && desktopReady ? "is-ready" : "is-waiting")} />
190
- <span>{backendReady && desktopReady ? "Ready" : "Starting"}</span>
191
- </button>
192
- <Button variant="outline" onClick={() => setPalette(true)}><Command className="h-4 w-4" /> Find</Button>
193
- <Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme">
194
- {theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
195
- </Button>
196
- </div>
197
- </header>
942
+ <div className="brain-graph-focus">
943
+ {selected ? (
944
+ <>
945
+ <span>{selected.type}</span>
946
+ <strong>{selected.label}</strong>
947
+ <p>{selected.summary || "This concept is part of the deepest knowledge layer."}</p>
948
+ </>
949
+ ) : (
950
+ <p>Capture documents, conversations, or projects to grow the graph.</p>
951
+ )}
952
+ </div>
953
+ </section>
954
+ );
955
+ }
198
956
 
199
- {drawer ? (
200
- <div className="mobile-drawer">
201
- <button className="drawer-scrim" aria-label="Close navigation" onClick={() => setDrawer(false)} />
202
- <div className="drawer-panel">
203
- <div className="drawer-header">
204
- <div>
205
- <div className="font-semibold">Lattice</div>
206
- <div className="text-xs text-muted-foreground">Choose a layer</div>
207
- </div>
208
- <Button variant="ghost" size="icon" onClick={() => setDrawer(false)} aria-label="Close navigation"><X className="h-4 w-4" /></Button>
209
- </div>
210
- <PrimaryDock active={route.primary} onNavigate={() => setDrawer(false)} />
211
- </div>
212
- </div>
213
- ) : null}
957
+ function buildMemoryFragments(memoryData: unknown, historyData: unknown): MemoryFragment[] {
958
+ const memory = isRecord(memoryData) ? memoryData : {};
959
+ const sourceRows = asArray<ApiRecord>(memory.sources).length
960
+ ? asArray<ApiRecord>(memory.sources)
961
+ : asArray<ApiRecord>(memory.tiers);
962
+ const sourceFragments = sourceRows.map((item, index) => ({
963
+ id: textValue(item, ["id", "source", "label"], `memory-${index}`),
964
+ title: textValue(item, ["title", "label", "source", "path", "name"], "Workspace memory"),
965
+ kind: titleValue(item, ["type", "source_type", "kind", "health"], "Memory"),
966
+ }));
967
+ const conversationFragments = asArray<ApiRecord>(historyData).map((item, index) => ({
968
+ id: textValue(item, ["id", "conversation_id"], `conversation-${index}`),
969
+ title: textValue(item, ["title", "summary", "id"], "Conversation"),
970
+ kind: "Conversation",
971
+ }));
214
972
 
215
- <main className="page-shell">
216
- <Page primary={route.primary} tab={route.tab} />
217
- </main>
218
- </div>
973
+ return uniqueById([...sourceFragments, ...conversationFragments]).slice(0, 10);
974
+ }
975
+
976
+ function parseKnowledgeGraph(data: unknown): KnowledgeGraphModel {
977
+ const graph = isRecord(data) ? data : {};
978
+ const rawNodes = asArray<ApiRecord>(graph.nodes);
979
+ const rawEdges = asArray<ApiRecord>(graph.edges);
980
+ const nodes = rawNodes.flatMap((node): KnowledgeConcept[] => {
981
+ const id = textValue(node, ["id", "node_id", "title", "label"]);
982
+ if (!id) return [];
983
+ const metadata = isRecord(node.metadata) ? node.metadata : {};
984
+ const type = titleValue(node, ["type", "kind", "category"], "Concept");
985
+ const label = textValue(node, ["title", "label", "name"], id.replace(/^[^:]+:/, ""));
986
+ const summary = textValue(node, ["summary", "description", "snippet"]) || textValue(metadata, ["summary", "description", "relative_path", "filename"]);
987
+ const importance = clamp(numberValue(node, ["importance_norm", "importance", "score"]) || 0.5, 0.08, 1);
988
+ return [{ id, label, type, summary, importance }];
989
+ }).sort((left, right) => right.importance - left.importance);
990
+ const ids = new Set(nodes.map((node) => node.id));
991
+ const edges = rawEdges.flatMap((edge, index): RelationshipThread[] => {
992
+ const source = textValue(edge, ["from", "source", "source_id"]);
993
+ const target = textValue(edge, ["to", "target", "target_id"]);
994
+ if (!source || !target || !ids.has(source) || !ids.has(target)) return [];
995
+ return [{
996
+ id: textValue(edge, ["id"], `edge-${index}`),
997
+ source,
998
+ target,
999
+ label: titleValue(edge, ["type", "label", "relationship"], "Relates"),
1000
+ weight: numberValue(edge, ["weight", "score", "confidence"]) || 1,
1001
+ }];
1002
+ });
1003
+ return { nodes, edges };
1004
+ }
1005
+
1006
+ function currentModelName(data: unknown) {
1007
+ const record = isRecord(data) ? data : {};
1008
+ const current = textValue(record, ["current", "current_model", "local_model"]);
1009
+ if (current) return current;
1010
+ const loaded = asArray<ApiRecord>(record.loaded || record.loaded_models);
1011
+ const firstLoaded = loaded.find((item) => item.id || item.name || item.model_id);
1012
+ return firstLoaded ? textValue(firstLoaded, ["name", "id", "model_id"], "local mind") : "local mind";
1013
+ }
1014
+
1015
+ function portabilityLabel(data: unknown) {
1016
+ const record = isRecord(data) ? data : {};
1017
+ return textValue(record, ["archive_format", "format", "graph_schema_version", "schema_version"], ".latticebrain");
1018
+ }
1019
+
1020
+ function backupHealthLabel(data: unknown) {
1021
+ const record = isRecord(data) ? data : {};
1022
+ const count = record.count || record.backups || record.available;
1023
+ if (count !== undefined && count !== null && count !== "") return `${count} backups`;
1024
+ return "Backups ready";
1025
+ }
1026
+
1027
+ function summarizeCareResult(result: ApiResult) {
1028
+ if (!result.ok) return result.error || "Brain care action could not complete.";
1029
+ const data = isRecord(result.data) ? result.data : {};
1030
+ const directMessage = textValue(data, ["message", "status", "path", "archive_path", "backup_path", "export_path"]);
1031
+ if (directMessage) return directMessage;
1032
+ return "Brain care action completed.";
1033
+ }
1034
+
1035
+ function renderLogRow(event: ApiRecord) {
1036
+ const action = stringValue(event.action || event.event || event.type || event.name, "Event");
1037
+ const actor = stringValue(event.actor || event.user || event.user_id || event.workspace_id, "system");
1038
+ const when = stringValue(event.timestamp || event.time || event.created_at || event.ts, "recently");
1039
+ return (
1040
+ <>
1041
+ <strong>{action}</strong>
1042
+ <span>{actor} · {when}</span>
1043
+ </>
219
1044
  );
220
1045
  }
1046
+
1047
+ function sourceLabel(result?: ApiResult<unknown>) {
1048
+ if (!result) return "Loading";
1049
+ return result.ok ? "Live" : result.error || "Unavailable";
1050
+ }
1051
+
1052
+ function adminStatusLabel(data: unknown, key: string) {
1053
+ const record = isRecord(data) ? data : {};
1054
+ return textValue(record, [key, "health", "state", "overall_status"]);
1055
+ }
1056
+
1057
+ function indexDetail(data: unknown) {
1058
+ const record = isRecord(data) ? data : {};
1059
+ const docs = record.documents ?? record.document_count ?? record.docs;
1060
+ const chunks = record.chunks ?? record.chunk_count ?? record.vectors;
1061
+ if (docs !== undefined || chunks !== undefined) {
1062
+ return `${stringValue(docs, "0")} docs · ${stringValue(chunks, "0")} chunks`;
1063
+ }
1064
+ return textValue(record, ["message", "detail", "status"], "Index status ready");
1065
+ }
1066
+
1067
+ function summaryText(data: unknown) {
1068
+ const record = isRecord(data) ? data : {};
1069
+ return textValue(record, ["summary", "message", "status", "detail"]);
1070
+ }
1071
+
1072
+ function stringValue(value: unknown, fallback = "") {
1073
+ if (typeof value === "string" && value.trim()) return value;
1074
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1075
+ if (typeof value === "boolean") return value ? "true" : "false";
1076
+ return fallback;
1077
+ }
1078
+
1079
+ function fileToDataUrl(file: File) {
1080
+ return new Promise<string>((resolve, reject) => {
1081
+ const reader = new FileReader();
1082
+ reader.onload = () => resolve(String(reader.result || ""));
1083
+ reader.onerror = () => reject(reader.error);
1084
+ reader.readAsDataURL(file);
1085
+ });
1086
+ }
1087
+
1088
+ function layoutGraphNodes(nodes: KnowledgeConcept[], radiusX: number, radiusY: number) {
1089
+ return nodes.map((node, index) => {
1090
+ const point = polarPoint(index, nodes.length, radiusX, radiusY, -88);
1091
+ return { node, x: point.x, y: point.y };
1092
+ });
1093
+ }
1094
+
1095
+ function polarPoint(index: number, total: number, radiusX: number, radiusY: number, offsetDegrees = -90) {
1096
+ const count = Math.max(total, 1);
1097
+ const angle = ((360 / count) * index + offsetDegrees) * Math.PI / 180;
1098
+ return {
1099
+ x: 50 + Math.cos(angle) * radiusX,
1100
+ y: 50 + Math.sin(angle) * radiusY,
1101
+ };
1102
+ }
1103
+
1104
+ function layerStyle(values: Record<string, string>) {
1105
+ return values as React.CSSProperties;
1106
+ }
1107
+
1108
+ function uniqueById<T extends { id: string }>(items: T[]) {
1109
+ const seen = new Set<string>();
1110
+ return items.filter((item) => {
1111
+ if (seen.has(item.id)) return false;
1112
+ seen.add(item.id);
1113
+ return true;
1114
+ });
1115
+ }
1116
+
1117
+ function isRecord(value: unknown): value is ApiRecord {
1118
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1119
+ }
1120
+
1121
+ function textValue(record: ApiRecord, keys: string[], fallback = "") {
1122
+ for (const key of keys) {
1123
+ const value = record[key];
1124
+ if (typeof value === "string" && value.trim()) return value;
1125
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1126
+ }
1127
+ return fallback;
1128
+ }
1129
+
1130
+ function titleValue(record: ApiRecord, keys: string[], fallback = "") {
1131
+ const value = textValue(record, keys, fallback);
1132
+ return value
1133
+ .replace(/[_-]+/g, " ")
1134
+ .replace(/\b\w/g, (character) => character.toUpperCase());
1135
+ }
1136
+
1137
+ function numberValue(record: ApiRecord, keys: string[]) {
1138
+ for (const key of keys) {
1139
+ const value = Number(record[key]);
1140
+ if (Number.isFinite(value)) return value;
1141
+ }
1142
+ return 0;
1143
+ }
1144
+
1145
+ function clamp(value: number, min: number, max: number) {
1146
+ return Math.max(min, Math.min(max, value));
1147
+ }