ltcai 4.4.0 → 4.6.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 (67) hide show
  1. package/README.md +77 -33
  2. package/docs/CHANGELOG.md +128 -0
  3. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  4. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  5. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  6. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  7. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  8. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  9. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  10. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  11. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  12. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  13. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  14. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  15. package/docs/V4_5_1_UX_REPORT.md +45 -0
  16. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  17. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  18. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +58 -0
  19. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -17
  20. package/docs/architecture.md +8 -4
  21. package/frontend/index.html +2 -2
  22. package/frontend/src/App.tsx +120 -98
  23. package/frontend/src/api/client.ts +84 -1
  24. package/frontend/src/components/BrainConversation.tsx +301 -0
  25. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  26. package/frontend/src/components/LivingBrain.tsx +121 -0
  27. package/frontend/src/components/ProductFlow.tsx +596 -0
  28. package/frontend/src/components/primitives.tsx +131 -25
  29. package/frontend/src/components/ui/badge.tsx +2 -2
  30. package/frontend/src/components/ui/button.tsx +7 -7
  31. package/frontend/src/components/ui/card.tsx +5 -5
  32. package/frontend/src/components/ui/input.tsx +1 -1
  33. package/frontend/src/components/ui/textarea.tsx +1 -1
  34. package/frontend/src/pages/Act.tsx +58 -28
  35. package/frontend/src/pages/Ask.tsx +2 -197
  36. package/frontend/src/pages/Brain.tsx +108 -71
  37. package/frontend/src/pages/Capture.tsx +24 -24
  38. package/frontend/src/pages/Library.tsx +222 -32
  39. package/frontend/src/pages/System.tsx +56 -34
  40. package/frontend/src/routes.ts +16 -25
  41. package/frontend/src/store/appStore.ts +8 -1
  42. package/frontend/src/styles.css +1663 -36
  43. package/lattice_brain/__init__.py +1 -1
  44. package/lattice_brain/runtime/multi_agent.py +1 -1
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/models.py +107 -18
  47. package/latticeai/core/marketplace.py +1 -1
  48. package/latticeai/core/model_compat.py +250 -0
  49. package/latticeai/core/workspace_os.py +1 -1
  50. package/latticeai/models/router.py +136 -32
  51. package/latticeai/services/model_catalog.py +2 -2
  52. package/latticeai/services/model_recommendation.py +8 -1
  53. package/latticeai/services/model_runtime.py +18 -3
  54. package/package.json +2 -2
  55. package/scripts/build_frontend_assets.mjs +12 -1
  56. package/src-tauri/Cargo.lock +1 -1
  57. package/src-tauri/Cargo.toml +1 -1
  58. package/src-tauri/tauri.conf.json +1 -1
  59. package/static/app/asset-manifest.json +5 -5
  60. package/static/app/assets/index-By-G-Kay.css +2 -0
  61. package/static/app/assets/index-CJx6WuQH.js +336 -0
  62. package/static/app/assets/index-CJx6WuQH.js.map +1 -0
  63. package/static/app/index.html +4 -4
  64. package/static/manifest.json +1 -1
  65. package/static/app/assets/index-CHHal8Zl.css +0 -2
  66. package/static/app/assets/index-pdzil9ac.js +0 -333
  67. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -0,0 +1,301 @@
1
+ import * as React from "react";
2
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { ImagePlus, MessageSquare, Plus, Send, Trash2 } from "lucide-react";
4
+ import { latticeApi } from "@/api/client";
5
+ import { EmptyState, EntityList, SourceBadge, StructuredView } from "@/components/primitives";
6
+ import { LivingBrain, type BrainActivity, type BrainVitals } from "@/components/LivingBrain";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Button } from "@/components/ui/button";
9
+ import { Textarea } from "@/components/ui/textarea";
10
+ import { asArray } from "@/lib/utils";
11
+
12
+ type Msg = { role?: string; content?: string; timestamp?: string };
13
+
14
+ function fileToDataUrl(file: File) {
15
+ return new Promise<string>((resolve, reject) => {
16
+ const reader = new FileReader();
17
+ reader.onload = () => resolve(String(reader.result || ""));
18
+ reader.onerror = () => reject(reader.error);
19
+ reader.readAsDataURL(file);
20
+ });
21
+ }
22
+
23
+ function newConversationId() {
24
+ const suffix = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.round(Math.random() * 10000)}`;
25
+ return `brain-${suffix}`;
26
+ }
27
+
28
+ function currentModelName(data: unknown) {
29
+ const record = data && typeof data === "object" ? data as Record<string, unknown> : {};
30
+ if (typeof record.current === "string" && record.current) return record.current;
31
+ const loaded = asArray<Record<string, unknown>>(record.loaded);
32
+ const firstLoaded = loaded.find((item) => item.id || item.name || item.model_id);
33
+ if (firstLoaded) return String(firstLoaded.name || firstLoaded.id || firstLoaded.model_id);
34
+ return null;
35
+ }
36
+
37
+ function usageNumber(data: unknown, key: string) {
38
+ const record = data && typeof data === "object" ? data as Record<string, unknown> : {};
39
+ const usage = record.usage && typeof record.usage === "object" ? record.usage as Record<string, unknown> : {};
40
+ const value = Number(usage[key] ?? record[key]);
41
+ return Number.isFinite(value) ? value : null;
42
+ }
43
+
44
+ export function BrainConversation({ className }: { className?: string }) {
45
+ const qc = useQueryClient();
46
+ const history = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
47
+ const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
48
+ const memory = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
49
+ const agentRuntime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime, refetchInterval: 12000 });
50
+ const [conversationId, setConversationId] = React.useState<string | null>(null);
51
+ const [selectedConversationId, setSelectedConversationId] = React.useState<string | null>(null);
52
+ const conversation = useQuery({
53
+ queryKey: ["conversation", selectedConversationId],
54
+ queryFn: () => latticeApi.conversation(selectedConversationId || ""),
55
+ enabled: !!selectedConversationId,
56
+ });
57
+ const [messages, setMessages] = React.useState<Msg[]>([]);
58
+ const [draft, setDraft] = React.useState("");
59
+ const [imageData, setImageData] = React.useState<string | null>(null);
60
+ const [trace, setTrace] = React.useState<unknown>(null);
61
+ const [streaming, setStreaming] = React.useState(false);
62
+
63
+ React.useEffect(() => {
64
+ if (conversation.data?.ok) {
65
+ setMessages(asArray<Msg>((conversation.data.data as Record<string, unknown>).messages || conversation.data.data));
66
+ }
67
+ }, [conversation.data]);
68
+
69
+ const send = async () => {
70
+ const message = draft.trim();
71
+ if (!message || streaming) return;
72
+ const activeConversationId = conversationId || newConversationId();
73
+ if (!conversationId) setConversationId(activeConversationId);
74
+ setDraft("");
75
+ setMessages((items) => [...items, { role: "user", content: message }, { role: "assistant", content: "" }]);
76
+ setStreaming(true);
77
+ try {
78
+ const result = await latticeApi.streamChat(
79
+ { message, conversation_id: activeConversationId, image_data: imageData || undefined },
80
+ {
81
+ onChunk: (_delta, fullText) => {
82
+ setMessages((items) => {
83
+ const next = [...items];
84
+ const last = next[next.length - 1] || { role: "assistant" };
85
+ next[next.length - 1] = { ...last, role: "assistant", content: fullText };
86
+ return next;
87
+ });
88
+ },
89
+ onTrace: setTrace,
90
+ },
91
+ );
92
+ if (result.error) {
93
+ setMessages((items) => {
94
+ const next = [...items];
95
+ next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
96
+ return next;
97
+ });
98
+ }
99
+ } finally {
100
+ setStreaming(false);
101
+ setImageData(null);
102
+ await qc.invalidateQueries({ queryKey: ["chatHistory"] });
103
+ await qc.invalidateQueries({ queryKey: ["memoryManager"] });
104
+ }
105
+ };
106
+
107
+ const deleteMutation = useMutation({
108
+ mutationFn: (id: string) => latticeApi.deleteConversation(id),
109
+ onSuccess: async (_result, id) => {
110
+ if (conversationId === id) {
111
+ setConversationId(null);
112
+ setSelectedConversationId(null);
113
+ setMessages([]);
114
+ }
115
+ await qc.invalidateQueries({ queryKey: ["chatHistory"] });
116
+ },
117
+ });
118
+
119
+ const historyItems = asArray<Record<string, unknown>>(history.data?.data);
120
+ const memoryData = memory.data?.data;
121
+ const runtimeData = agentRuntime.data?.data as Record<string, unknown> | undefined;
122
+ const runs = asArray<Record<string, unknown>>(runtimeData?.runs);
123
+ const activity: BrainActivity =
124
+ streaming ? "thinking" :
125
+ imageData ? "recalling" :
126
+ runs.some((run) => String(run.status || run.state || "").match(/running|active|queued/i)) ? "acting" :
127
+ draft.trim().length > 2 ? "listening" :
128
+ trace ? "recalling" :
129
+ "idle";
130
+ const vitals: BrainVitals = {
131
+ connected: Boolean(models.data?.ok),
132
+ memories: usageNumber(memoryData, "total_items") ?? asArray((memoryData as Record<string, unknown> | undefined)?.sources).length,
133
+ knowledge: usageNumber(memoryData, "sources") ?? asArray((memoryData as Record<string, unknown> | undefined)?.tiers).length,
134
+ conversations: historyItems.length,
135
+ model: currentModelName(models.data?.data),
136
+ };
137
+
138
+ return (
139
+ <div className={className}>
140
+ <div className="brain-conversation-grid">
141
+ <section className="brain-presence-column" aria-label="Living Brain presence">
142
+ <LivingBrain activity={activity} vitals={vitals} />
143
+ </section>
144
+
145
+ <section className="brain-chat-panel premium-surface" aria-label="Conversation with Lattice Brain">
146
+ <div className="brain-chat-head">
147
+ <div>
148
+ <div className="brain-chat-kicker"><MessageSquare className="h-4 w-4" /> Conversation</div>
149
+ <h1>Talk to your Brain.</h1>
150
+ </div>
151
+ <div className="brain-chat-model">
152
+ <Badge variant="muted">{currentModelName(models.data?.data) || "model readying"}</Badge>
153
+ <SourceBadge result={models.data} />
154
+ </div>
155
+ </div>
156
+
157
+ <div className="brain-message-stream soft-scrollbar">
158
+ {messages.length ? messages.map((msg, index) => (
159
+ <div key={`${msg.role || "message"}-${index}`} className={`brain-message-row ${msg.role === "user" ? "from-user" : "from-brain"}`}>
160
+ <div className="brain-message-bubble">
161
+ <div className="brain-message-role">{msg.role === "user" ? "You" : "Brain"}</div>
162
+ <div className="whitespace-pre-wrap">{msg.content}</div>
163
+ </div>
164
+ </div>
165
+ )) : (
166
+ <div className="brain-empty-conversation">
167
+ <EmptyState
168
+ title="What should we think through?"
169
+ detail="Bring a question, a project, or a loose thought. Lattice will answer through your private memory when a model is ready."
170
+ />
171
+ </div>
172
+ )}
173
+ </div>
174
+
175
+ <div className="brain-composer">
176
+ {imageData ? <Badge variant="success" className="mb-2">image attached</Badge> : null}
177
+ <Textarea
178
+ value={draft}
179
+ onChange={(event) => setDraft(event.target.value)}
180
+ onKeyDown={(event) => {
181
+ if (event.key === "Enter" && !event.shiftKey) {
182
+ event.preventDefault();
183
+ void send();
184
+ }
185
+ }}
186
+ placeholder="Ask the Brain anything..."
187
+ />
188
+ <div className="brain-composer-actions">
189
+ <label className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted">
190
+ <ImagePlus className="h-4 w-4" />
191
+ Image
192
+ <input
193
+ type="file"
194
+ accept="image/*"
195
+ className="sr-only"
196
+ onChange={async (event) => {
197
+ const file = event.target.files?.[0];
198
+ if (file) setImageData(await fileToDataUrl(file));
199
+ }}
200
+ />
201
+ </label>
202
+ <Button disabled={!draft.trim() || streaming} onClick={() => void send()}>
203
+ <Send className="h-4 w-4" /> Send
204
+ </Button>
205
+ </div>
206
+ </div>
207
+ </section>
208
+
209
+ <aside className="brain-context-column" aria-label="Conversation memory">
210
+ <RecentConversations
211
+ conversations={historyItems}
212
+ result={history.data}
213
+ activeId={conversationId}
214
+ onNew={() => {
215
+ setConversationId(null);
216
+ setSelectedConversationId(null);
217
+ setMessages([]);
218
+ setTrace(null);
219
+ }}
220
+ onSelect={(id) => {
221
+ setConversationId(id);
222
+ setSelectedConversationId(id);
223
+ setTrace(null);
224
+ }}
225
+ onDelete={(id) => deleteMutation.mutate(id)}
226
+ />
227
+ <MemoryNearby question={draft || [...messages].reverse().find((msg) => msg.role === "user")?.content || ""} trace={trace} />
228
+ </aside>
229
+ </div>
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function RecentConversations({
235
+ conversations,
236
+ result,
237
+ activeId,
238
+ onNew,
239
+ onSelect,
240
+ onDelete,
241
+ }: {
242
+ conversations: Array<Record<string, unknown>>;
243
+ result?: Parameters<typeof SourceBadge>[0]["result"];
244
+ activeId: string | null;
245
+ onNew: () => void;
246
+ onSelect: (id: string) => void;
247
+ onDelete: (id: string) => void;
248
+ }) {
249
+ return (
250
+ <section className="brain-side-panel">
251
+ <div className="brain-side-head">
252
+ <div>
253
+ <h3>Recent conversations</h3>
254
+ <SourceBadge result={result} />
255
+ </div>
256
+ <Button variant="outline" size="sm" onClick={onNew}><Plus className="h-4 w-4" /> New</Button>
257
+ </div>
258
+ <div className="brain-conversation-list soft-scrollbar">
259
+ {conversations.length ? conversations.slice(0, 8).map((item) => {
260
+ const id = String(item.id || item.conversation_id || "");
261
+ return (
262
+ <div key={id} className={`brain-conversation-item ${activeId === id ? "is-active" : ""}`}>
263
+ <button onClick={() => onSelect(id)} className="min-w-0 text-left">
264
+ <span>{String(item.title || id || "Conversation")}</span>
265
+ <small>{String(item.updated_at || item.started_at || "")}</small>
266
+ </button>
267
+ <button className="brain-delete-button" onClick={() => onDelete(id)} aria-label="Delete conversation">
268
+ <Trash2 className="h-3.5 w-3.5" />
269
+ </button>
270
+ </div>
271
+ );
272
+ }) : <EmptyState title="No conversations yet" detail="New exchanges will appear here." />}
273
+ </div>
274
+ </section>
275
+ );
276
+ }
277
+
278
+ function MemoryNearby({ question, trace }: { question: string; trace: unknown }) {
279
+ const hybrid = useQuery({
280
+ queryKey: ["brainNearbyMemory", question],
281
+ queryFn: () => latticeApi.hybridSearch(question),
282
+ enabled: question.trim().length > 2,
283
+ });
284
+ return (
285
+ <section className="brain-side-panel">
286
+ <div className="brain-side-head">
287
+ <div>
288
+ <h3>Memory nearby</h3>
289
+ <SourceBadge result={hybrid.data} />
290
+ </div>
291
+ </div>
292
+ {hybrid.data?.ok ? (
293
+ <EntityList items={(hybrid.data.data as Record<string, unknown>).matches || hybrid.data.data} titleKey="title" metaKey="type" limit={4} />
294
+ ) : trace ? (
295
+ <StructuredView value={trace} limit={4} />
296
+ ) : (
297
+ <EmptyState title="Quiet for now" detail="Relevant memory wakes up as the conversation gets specific." />
298
+ )}
299
+ </section>
300
+ );
301
+ }
@@ -0,0 +1,99 @@
1
+ import * as React from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { ArrowRight, CheckCircle2, Cpu, Download, Layers3, Library, PlayCircle, SlidersHorizontal, UserCircle, Users } from "lucide-react";
4
+ import { latticeApi } from "@/api/client";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { useAppStore } from "@/store/appStore";
8
+ import { go } from "@/routes";
9
+ import { asArray } from "@/lib/utils";
10
+
11
+ function readDismissed() {
12
+ try {
13
+ return localStorage.getItem("lattice.onboarding.dismissed") === "true";
14
+ } catch {}
15
+ return false;
16
+ }
17
+
18
+ export function FirstRunGuide() {
19
+ const [dismissed, setDismissed] = React.useState(readDismissed);
20
+ const mode = useAppStore((state) => state.mode);
21
+ const profile = useQuery({ queryKey: ["profile"], queryFn: latticeApi.profile });
22
+ const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
23
+ const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
24
+ const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
25
+ if (dismissed) return null;
26
+
27
+ const profileData = (profile.data?.data || {}) as Record<string, unknown>;
28
+ const workspaceData = (workspace.data?.data || {}) as Record<string, unknown>;
29
+ const registry = (workspaceData.workspace_registry || {}) as Record<string, unknown>;
30
+ const modelData = (models.data?.data || {}) as Record<string, unknown>;
31
+ const recommendationData = ((recs.data?.data as Record<string, unknown> | undefined)?.recommendations || {}) as Record<string, unknown>;
32
+ const currentModel = String(modelData.current || "");
33
+ const loadedModels = asArray(modelData.loaded);
34
+ const topPick = recommendationData.top_pick as Record<string, unknown> | undefined;
35
+ const compatProfiles = asArray<Record<string, unknown>>(modelData.compat_profiles);
36
+ const readyProfile = compatProfiles.some((item) => item.chat_compatible || item.quality_status === "ok" || item.quality_status === "degraded");
37
+
38
+ const steps = [
39
+ { label: "Make it yours", done: Boolean(profileData.email), icon: UserCircle, action: "account", detail: "Sign in or keep a local profile." },
40
+ { label: "Choose a space", done: Boolean(registry.active_workspace || workspaceData.active_workspace), icon: Users, action: "workspace-admin", detail: "Decide where memories belong." },
41
+ { label: "Meet your Mac", done: recs.isSuccess, icon: Cpu, action: "models", detail: "Let Lattice inspect what can run locally." },
42
+ { label: "Pick a brain", done: Boolean(topPick || currentModel), icon: Library, action: "models", detail: "Use the recommended local model." },
43
+ { label: "Install locally", done: Boolean(currentModel || loadedModels.length), icon: Download, action: "models", detail: "Download only with explicit consent." },
44
+ { label: "Talk to Brain", done: Boolean(readyProfile || currentModel || loadedModels.length), icon: PlayCircle, action: "chat", detail: "Confirm the model can answer." },
45
+ { label: "Set the pace", done: Boolean(mode), icon: SlidersHorizontal, action: "settings", detail: "Stay Calm or switch deeper." },
46
+ { label: "Explore deeply", done: true, icon: Layers3, action: "knowledge-graph", detail: "Open advanced relationships." },
47
+ ];
48
+ const completed = steps.filter((step) => step.done).length;
49
+ const nextStep = steps.find((step) => !step.done) || steps[steps.length - 1];
50
+ const progress = Math.round((completed / steps.length) * 100);
51
+
52
+ return (
53
+ <section className="arrival-panel" aria-label="First 10 minutes">
54
+ <div className="arrival-copy">
55
+ <div className="page-kicker"><CheckCircle2 className="h-4 w-4" /> First 10 minutes</div>
56
+ <h2>Build your living Brain without guessing.</h2>
57
+ <p>
58
+ Start with a space, let Lattice recommend a private local model, then add the first pieces of knowledge.
59
+ Every step keeps the next action visible.
60
+ </p>
61
+ <div className="arrival-actions">
62
+ <Button onClick={() => go(nextStep.action)}>{nextStep.done ? "Open relationships" : `Continue: ${nextStep.label}`}</Button>
63
+ <Button variant="outline" onClick={() => go("models")}>Set up model</Button>
64
+ <Button variant="ghost" onClick={() => {
65
+ try { localStorage.setItem("lattice.onboarding.dismissed", "true"); } catch {}
66
+ setDismissed(true);
67
+ }}>
68
+ Hide
69
+ </Button>
70
+ </div>
71
+ </div>
72
+ <div className="journey-panel">
73
+ <div className="journey-head">
74
+ <div>
75
+ <div className="text-sm font-semibold">{completed} of {steps.length} ready</div>
76
+ <div className="text-xs text-muted-foreground">{mode === "basic" ? "Calm mode" : `${mode} mode`}</div>
77
+ </div>
78
+ <Badge variant={progress === 100 ? "success" : "warning"}>{progress}%</Badge>
79
+ </div>
80
+ <div className="journey-progress"><span style={{ width: `${progress}%` }} /></div>
81
+ <div className="journey-steps">
82
+ {steps.map((step) => {
83
+ const Icon = step.icon;
84
+ return (
85
+ <button key={step.label} onClick={() => go(step.action)} className="journey-step">
86
+ <span className="journey-icon"><Icon className="h-4 w-4" /></span>
87
+ <span className="min-w-0">
88
+ <span className="block truncate text-sm font-semibold">{step.label}</span>
89
+ <span className="block truncate text-xs text-muted-foreground">{step.detail}</span>
90
+ </span>
91
+ <ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
92
+ </button>
93
+ );
94
+ })}
95
+ </div>
96
+ </div>
97
+ </section>
98
+ );
99
+ }
@@ -0,0 +1,121 @@
1
+ import * as React from "react";
2
+ import { Activity, Brain, CheckCircle2, CircleDotDashed, Loader2, Sparkles, Waves } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { cn, fmtNumber } from "@/lib/utils";
5
+
6
+ export type BrainActivity = "idle" | "listening" | "recalling" | "thinking" | "planning" | "acting";
7
+
8
+ export type BrainVitals = {
9
+ connected?: boolean;
10
+ memories?: number | string | null;
11
+ knowledge?: number | string | null;
12
+ conversations?: number | string | null;
13
+ model?: string | null;
14
+ activityLabel?: string | null;
15
+ };
16
+
17
+ const activityLabels: Record<BrainActivity, string> = {
18
+ idle: "Present",
19
+ listening: "Listening",
20
+ recalling: "Recalling",
21
+ thinking: "Thinking",
22
+ planning: "Planning",
23
+ acting: "Acting",
24
+ };
25
+
26
+ function readable(value: BrainVitals[keyof BrainVitals]) {
27
+ if (value === null || value === undefined || value === "") return "-";
28
+ if (typeof value === "number") return fmtNumber(value);
29
+ return String(value);
30
+ }
31
+
32
+ export function LivingBrain({
33
+ activity = "idle",
34
+ vitals,
35
+ compact = false,
36
+ showVitals = false,
37
+ className,
38
+ }: {
39
+ activity?: BrainActivity;
40
+ vitals?: BrainVitals;
41
+ compact?: boolean;
42
+ showVitals?: boolean;
43
+ className?: string;
44
+ }) {
45
+ const state = vitals?.activityLabel || activityLabels[activity];
46
+ const connected = vitals?.connected !== false;
47
+
48
+ return (
49
+ <section
50
+ className={cn("living-brain", compact && "living-brain-compact", className)}
51
+ data-activity={activity}
52
+ aria-label="Living Brain"
53
+ >
54
+ <div className="brain-presence-head">
55
+ <div>
56
+ <div className="brain-presence-kicker"><Brain className="h-4 w-4" /> Lattice Brain</div>
57
+ <h2>The Brain is awake.</h2>
58
+ </div>
59
+ <Badge variant={connected ? "success" : "warning"}>{connected ? "online" : "starting"}</Badge>
60
+ </div>
61
+
62
+ <div className="brain-stage" aria-hidden="true">
63
+ <span className="brain-halo brain-halo-a" />
64
+ <span className="brain-halo brain-halo-b" />
65
+ <span className="brain-wave brain-wave-a" />
66
+ <span className="brain-wave brain-wave-b" />
67
+ <span className="brain-wave brain-wave-c" />
68
+ <svg className="brain-organ" viewBox="0 0 440 360" role="img" aria-label="Animated Brain presence">
69
+ <defs>
70
+ <filter id="brainGlow" x="-40%" y="-40%" width="180%" height="180%">
71
+ <feGaussianBlur stdDeviation="10" result="blur" />
72
+ <feColorMatrix in="blur" type="matrix" values="0 0 0 0 0.12 0 0 0 0 0.78 0 0 0 0 0.68 0 0 0 0.52 0" />
73
+ <feBlend in="SourceGraphic" />
74
+ </filter>
75
+ </defs>
76
+ <path className="brain-mass brain-mass-left" d="M214 74c-25-34-82-31-103 7-28 1-50 22-51 51-28 16-39 51-25 81-14 32 6 72 42 80 18 35 72 43 101 14 30 17 71 6 87-25 27-11 43-40 36-69 19-25 14-64-11-84 2-31-31-61-76-55Z" />
77
+ <path className="brain-mass brain-mass-right" d="M224 74c24-35 83-33 105 5 29 0 52 22 53 52 28 16 39 52 24 82 14 33-8 74-45 80-19 34-73 41-101 11-31 16-72 3-86-29-27-12-41-42-33-71-18-27-10-66 18-85-1-30 31-57 65-45Z" />
78
+ <path className="thought-path thought-path-a" d="M106 143c35-30 83-27 113 9 26 31 74 31 110 0" />
79
+ <path className="thought-path thought-path-b" d="M93 218c38 25 75 23 112-8 34-29 75-31 121-6" />
80
+ <path className="thought-path thought-path-c" d="M144 100c9 38 33 59 72 63 42 5 69 27 83 67" />
81
+ <path className="thought-path thought-path-d" d="M149 286c16-44 42-68 78-71 42-4 72-26 89-66" />
82
+ <circle className="memory-pulse pulse-a" cx="125" cy="150" r="7" />
83
+ <circle className="memory-pulse pulse-b" cx="223" cy="164" r="8" />
84
+ <circle className="memory-pulse pulse-c" cx="312" cy="210" r="7" />
85
+ <circle className="memory-pulse pulse-d" cx="183" cy="257" r="6" />
86
+ </svg>
87
+ <div className="brain-state-pill">
88
+ {activity === "thinking" ? <Loader2 className="h-4 w-4 animate-spin" /> : <CircleDotDashed className="h-4 w-4" />}
89
+ <span>{state}</span>
90
+ </div>
91
+ </div>
92
+
93
+ {showVitals ? (
94
+ <div className="brain-vitals">
95
+ <div className="brain-vital">
96
+ <Sparkles className="h-4 w-4" />
97
+ <span>Memories</span>
98
+ <strong>{readable(vitals?.memories)}</strong>
99
+ </div>
100
+ <div className="brain-vital">
101
+ <Waves className="h-4 w-4" />
102
+ <span>Knowledge</span>
103
+ <strong>{readable(vitals?.knowledge)}</strong>
104
+ </div>
105
+ <div className="brain-vital">
106
+ <Activity className="h-4 w-4" />
107
+ <span>Activity</span>
108
+ <strong>{readable(vitals?.conversations)}</strong>
109
+ </div>
110
+ </div>
111
+ ) : null}
112
+
113
+ {compact ? null : (
114
+ <div className="brain-presence-foot">
115
+ <CheckCircle2 className="h-4 w-4 text-primary" />
116
+ <span>{vitals?.model ? readable(vitals.model) : "Waiting for a local model"}</span>
117
+ </div>
118
+ )}
119
+ </section>
120
+ );
121
+ }