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
@@ -1,200 +1,5 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { ImagePlus, MessageSquare, Send, Trash2 } from "lucide-react";
4
- import { latticeApi } from "@/api/client";
5
- import { DataPanel, EmptyState, EntityList, SourceBadge, StructuredView } from "@/components/primitives";
6
- import { Badge } from "@/components/ui/badge";
7
- import { Button } from "@/components/ui/button";
8
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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
- }
1
+ import { BrainConversation } from "@/components/BrainConversation";
22
2
 
23
3
  export function AskPage() {
24
- const qc = useQueryClient();
25
- const history = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
26
- const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
27
- const [conversationId, setConversationId] = React.useState<string | null>(null);
28
- const conversation = useQuery({
29
- queryKey: ["conversation", conversationId],
30
- queryFn: () => latticeApi.conversation(conversationId || ""),
31
- enabled: !!conversationId,
32
- });
33
- const [messages, setMessages] = React.useState<Msg[]>([]);
34
- const [draft, setDraft] = React.useState("");
35
- const [imageData, setImageData] = React.useState<string | null>(null);
36
- const [trace, setTrace] = React.useState<unknown>(null);
37
- const [streaming, setStreaming] = React.useState(false);
38
-
39
- React.useEffect(() => {
40
- if (conversation.data?.ok) setMessages(asArray<Msg>((conversation.data.data as Record<string, unknown>).messages || conversation.data.data));
41
- }, [conversation.data]);
42
-
43
- const send = async () => {
44
- const message = draft.trim();
45
- if (!message || streaming) return;
46
- setDraft("");
47
- setMessages((items) => [...items, { role: "user", content: message }, { role: "assistant", content: "" }]);
48
- setStreaming(true);
49
- try {
50
- const result = await latticeApi.streamChat(
51
- { message, conversation_id: conversationId || undefined, image_data: imageData || undefined },
52
- {
53
- onChunk: (_delta, fullText) => {
54
- setMessages((items) => {
55
- const next = [...items];
56
- const last = next[next.length - 1] || { role: "assistant" };
57
- next[next.length - 1] = { ...last, role: "assistant", content: fullText };
58
- return next;
59
- });
60
- },
61
- onTrace: setTrace,
62
- },
63
- );
64
- if (result.error) {
65
- setMessages((items) => {
66
- const next = [...items];
67
- next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
68
- return next;
69
- });
70
- }
71
- } finally {
72
- setStreaming(false);
73
- setImageData(null);
74
- await qc.invalidateQueries({ queryKey: ["chatHistory"] });
75
- }
76
- };
77
-
78
- const deleteMutation = useMutation({
79
- mutationFn: (id: string) => latticeApi.deleteConversation(id),
80
- onSuccess: () => qc.invalidateQueries({ queryKey: ["chatHistory"] }),
81
- });
82
-
83
- return (
84
- <div className="grid min-h-[calc(100vh-7rem)] gap-4 xl:grid-cols-[18rem_minmax(0,1fr)_22rem]">
85
- <Card className="overflow-hidden">
86
- <CardHeader>
87
- <CardTitle className="flex items-center gap-2"><MessageSquare className="h-4 w-4" /> Conversations</CardTitle>
88
- <CardDescription>Durable history from the backend conversation store.</CardDescription>
89
- </CardHeader>
90
- <CardContent className="space-y-2">
91
- <SourceBadge result={history.data} />
92
- {asArray<Record<string, unknown>>(history.data?.data).length ? asArray<Record<string, unknown>>(history.data?.data).map((item) => (
93
- <button
94
- key={String(item.id)}
95
- onClick={() => setConversationId(String(item.id))}
96
- className="block w-full rounded-md border border-border bg-background p-3 text-left text-sm transition hover:bg-muted"
97
- >
98
- <div className="font-medium">{String(item.title || item.id)}</div>
99
- <div className="mt-1 flex items-center justify-between gap-2 text-xs text-muted-foreground">
100
- <span>{String(item.updated_at || "")}</span>
101
- <Trash2
102
- className="h-3.5 w-3.5"
103
- onClick={(e) => {
104
- e.stopPropagation();
105
- deleteMutation.mutate(String(item.id));
106
- }}
107
- />
108
- </div>
109
- </button>
110
- )) : <EmptyState title="No conversations" detail="Start a new exchange or sign in to load history." />}
111
- </CardContent>
112
- </Card>
113
-
114
- <section className="flex min-h-[40rem] flex-col rounded-lg border border-border bg-card">
115
- <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-4">
116
- <div>
117
- <h1 className="text-xl font-semibold">Ask</h1>
118
- <p className="text-sm text-muted-foreground">Streams through `/chat`; no local answer is fabricated when the model is unavailable.</p>
119
- </div>
120
- <div className="flex flex-wrap items-center gap-2">
121
- <Badge variant="muted">{String((models.data?.data as Record<string, unknown>)?.current || "no model loaded")}</Badge>
122
- <SourceBadge result={models.data} />
123
- </div>
124
- </div>
125
- <div className="scrollbar-thin flex-1 space-y-3 overflow-auto p-4">
126
- {messages.length ? messages.map((msg, index) => (
127
- <div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
128
- <div className={`max-w-[78%] rounded-lg border p-3 text-sm ${msg.role === "user" ? "border-primary/30 bg-primary/15" : "border-border bg-background"}`}>
129
- <div className="mb-1 text-xs uppercase text-muted-foreground">{msg.role || "message"}</div>
130
- <div className="whitespace-pre-wrap">{msg.content}</div>
131
- </div>
132
- </div>
133
- )) : <EmptyState title="Ready" detail="Ask a question once the backend and model are available." />}
134
- </div>
135
- <div className="border-t border-border p-4">
136
- {imageData ? <Badge variant="success" className="mb-2">image attached</Badge> : null}
137
- <Textarea
138
- value={draft}
139
- onChange={(e) => setDraft(e.target.value)}
140
- onKeyDown={(e) => {
141
- if (e.key === "Enter" && !e.shiftKey) {
142
- e.preventDefault();
143
- void send();
144
- }
145
- }}
146
- placeholder="Ask the brain..."
147
- />
148
- <div className="mt-2 flex flex-wrap items-center justify-between gap-2">
149
- <label className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted">
150
- <ImagePlus className="h-4 w-4" />
151
- Attach image
152
- <input
153
- type="file"
154
- accept="image/*"
155
- className="sr-only"
156
- onChange={async (e) => {
157
- const file = e.target.files?.[0];
158
- if (file) setImageData(await fileToDataUrl(file));
159
- }}
160
- />
161
- </label>
162
- <Button disabled={!draft.trim() || streaming} onClick={() => void send()}>
163
- <Send className="h-4 w-4" /> Send
164
- </Button>
165
- </div>
166
- </div>
167
- </section>
168
-
169
- <aside className="space-y-4">
170
- <ContextPreview question={draft || [...messages].reverse().find((m: Msg) => m.role === "user")?.content || ""} trace={trace} />
171
- </aside>
172
- </div>
173
- );
174
- }
175
-
176
- function ContextPreview({ question, trace }: { question: string; trace: unknown }) {
177
- const hybrid = useQuery({
178
- queryKey: ["askHybrid", question],
179
- queryFn: () => latticeApi.hybridSearch(question),
180
- enabled: question.trim().length > 2,
181
- });
182
- const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
183
- return (
184
- <>
185
- <DataPanel title="Retrieval preview" result={hybrid.data}>
186
- {(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={5} />}
187
- </DataPanel>
188
- <DataPanel title="Graph context" result={graph.data}>
189
- {(data) => <EntityList items={(data as Record<string, unknown>).nodes} titleKey="title" metaKey="type" limit={5} />}
190
- </DataPanel>
191
- <Card>
192
- <CardHeader>
193
- <CardTitle>Why this context</CardTitle>
194
- <CardDescription>Trace emitted by `/chat` when the backend includes it.</CardDescription>
195
- </CardHeader>
196
- <CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="No trace yet" />}</CardContent>
197
- </Card>
198
- </>
199
- );
4
+ return <BrainConversation />;
200
5
  }
@@ -3,15 +3,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import cytoscape, { Core, ElementDefinition } from "cytoscape";
4
4
  import { BrainCircuit, DatabaseBackup, Filter, Focus, Layers3, LocateFixed, Search, Sparkles } from "lucide-react";
5
5
  import { latticeApi } from "@/api/client";
6
- import { ActionButton, DataPanel, EmptyState, EntityList, LoadingPanel, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
6
+ import { BrainConversation } from "@/components/BrainConversation";
7
+ import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, LoadingPanel, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
7
8
  import { Badge } from "@/components/ui/badge";
8
9
  import { Button } from "@/components/ui/button";
9
10
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
11
  import { Input } from "@/components/ui/input";
11
12
  import { Textarea } from "@/components/ui/textarea";
13
+ import { useAppStore } from "@/store/appStore";
12
14
  import { asArray, fmtNumber, pct, shortId, titleize } from "@/lib/utils";
13
15
 
14
- type BrainTab = "overview" | "graph" | "search" | "memory" | "provenance" | "portability";
16
+ type BrainTab = "conversation" | "memory" | "knowledge" | "relationships" | "graph" | "portability";
15
17
  type LabelMode = "important" | "all" | "off";
16
18
 
17
19
  type GraphNode = {
@@ -60,12 +62,12 @@ type ExplorerModel = ParsedGraph & {
60
62
  };
61
63
 
62
64
  const tabs: Array<{ id: BrainTab; label: string }> = [
63
- { id: "overview", label: "Overview" },
65
+ { id: "conversation", label: "Brain" },
66
+ { id: "memory", label: "Memories" },
67
+ { id: "knowledge", label: "Knowledge" },
68
+ { id: "relationships", label: "Relationships" },
64
69
  { id: "graph", label: "Graph" },
65
- { id: "search", label: "Search" },
66
- { id: "memory", label: "Memory" },
67
- { id: "provenance", label: "Provenance" },
68
- { id: "portability", label: "Portability" },
70
+ { id: "portability", label: "Care" },
69
71
  ];
70
72
 
71
73
  const groupDefinitions = [
@@ -405,15 +407,17 @@ function CytoscapeGraph({
405
407
  <div
406
408
  ref={hostRef}
407
409
  data-testid="brain-cytoscape"
408
- className="h-[620px] min-h-[32rem] w-full overflow-hidden rounded-md border border-border bg-background brain-grid"
410
+ className="brain-grid h-[620px] min-h-[32rem] w-full overflow-hidden rounded-lg border border-border bg-background/80"
409
411
  />
410
412
  );
411
413
  }
412
414
 
413
415
  export function BrainPage({ initialTab }: { initialTab?: string }) {
414
- const [tab, setTab] = React.useState<BrainTab>((initialTab as BrainTab) || "graph");
416
+ const mode = useAppStore((state) => state.mode);
417
+ const normalizedInitialTab = normalizeBrainTab(initialTab);
418
+ const [tab, setTab] = React.useState<BrainTab>(normalizedInitialTab);
415
419
  React.useEffect(() => {
416
- if (initialTab && tabs.some((item) => item.id === initialTab)) setTab(initialTab as BrainTab);
420
+ setTab(normalizeBrainTab(initialTab));
417
421
  }, [initialTab]);
418
422
  const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
419
423
  const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
@@ -423,68 +427,90 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
423
427
  const memory = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
424
428
 
425
429
  return (
426
- <div className="space-y-4">
427
- <header className="grid gap-4 xl:grid-cols-[1.4fr_0.6fr]">
428
- <div>
429
- <div className="flex items-center gap-2 text-sm text-primary"><BrainCircuit className="h-4 w-4" /> Graph-first Digital Brain</div>
430
- <h1 className="mt-2 text-3xl font-semibold tracking-normal">Brain</h1>
431
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
432
- Explore the knowledge graph, memory, provenance, retrieval, and portable brain state. Empty states reflect live API availability.
433
- </p>
434
- </div>
435
- <div className="rounded-md border border-border bg-card p-4">
436
- <div className="text-xs uppercase text-muted-foreground">Provenance coverage</div>
437
- <div className="mt-2 text-3xl font-semibold">{pct((coverage.data?.data as Record<string, unknown>)?.coverage_ratio)}</div>
438
- <div className="mt-2 text-sm text-muted-foreground">Source: {coverage.data?.source || "loading"}</div>
439
- </div>
440
- </header>
430
+ <div className="space-y-5">
431
+ {tab === "conversation" ? null : (
432
+ <header className="brain-layer-header">
433
+ <div>
434
+ <div className="page-kicker"><BrainCircuit className="h-4 w-4" /> {tabLabel(tab)}</div>
435
+ <h1>{tabHeadline(tab)}</h1>
436
+ </div>
437
+ <div className="brain-layer-meter">
438
+ <span>Source coverage</span>
439
+ <strong>{pct((coverage.data?.data as Record<string, unknown>)?.coverage_ratio)}</strong>
440
+ </div>
441
+ </header>
442
+ )}
441
443
  <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as BrainTab)} />
442
444
 
443
- {tab === "overview" ? (
445
+ {tab === "conversation" ? <BrainConversation /> : null}
446
+ {tab === "memory" ? <MemoryPanel /> : null}
447
+ {tab === "knowledge" ? <HybridSearch /> : null}
448
+ {tab === "relationships" ? (
444
449
  <div className="grid gap-4 xl:grid-cols-2">
445
- <DataPanel title="Brain status" result={stats.data}>
450
+ <DataPanel title="Brain activity" result={stats.data}>
446
451
  {(data) => <GraphStatus data={data as Record<string, unknown>} />}
447
452
  </DataPanel>
448
- <DataPanel title="Retrieval index" result={index.data}>
453
+ <DataPanel title="Retrieval rhythm" result={index.data}>
449
454
  {(data) => <RetrievalStatus data={data as Record<string, unknown>} />}
450
455
  </DataPanel>
451
- <DataPanel title="Memory tiers" result={memory.data}>
456
+ <DataPanel title="Memory layers" result={memory.data}>
452
457
  {(data) => <MemoryStatus data={data as Record<string, unknown>} />}
453
458
  </DataPanel>
454
- <DataPanel title="Recent provenance" result={provenance.data}>
459
+ <DataPanel title="Recent sources" result={provenance.data}>
455
460
  {(data) => <EntityList items={(data as Record<string, unknown>).items || data} titleKey="source" metaKey="source_type" />}
456
461
  </DataPanel>
457
462
  </div>
458
463
  ) : null}
459
464
 
460
465
  {tab === "graph" ? (
461
- graph.isLoading ? <LoadingPanel title="Knowledge graph" /> : (
462
- <DataPanel title="Digital Brain explorer" description="Interactive Cytoscape.js explorer backed by /knowledge-graph/graph." result={graph.data}>
466
+ graph.isLoading ? <LoadingPanel title="Deep graph" /> : (
467
+ <DataPanel title="Advanced relationship graph" description={mode === "basic" ? "Open the deepest layer when you want to inspect the underlying relationships." : "Explore relationships, sources, and graph structure."} result={graph.data}>
463
468
  {(data) => <DigitalBrainExplorer data={data} />}
464
469
  </DataPanel>
465
470
  )
466
471
  ) : null}
467
-
468
- {tab === "search" ? <HybridSearch /> : null}
469
- {tab === "memory" ? <MemoryPanel /> : null}
470
- {tab === "provenance" ? <ProvenancePanel /> : null}
471
472
  {tab === "portability" ? <PortabilityPanel /> : null}
472
473
  </div>
473
474
  );
474
475
  }
475
476
 
477
+ function normalizeBrainTab(tab?: string): BrainTab {
478
+ if (tab === "overview" || tab === "chat" || tab === "ask") return "conversation";
479
+ if (tab === "search") return "knowledge";
480
+ if (tab === "provenance" || tab === "sources") return "relationships";
481
+ return tabs.some((item) => item.id === tab) ? tab as BrainTab : "conversation";
482
+ }
483
+
484
+ function tabLabel(tab: BrainTab) {
485
+ return tabs.find((item) => item.id === tab)?.label || "Brain";
486
+ }
487
+
488
+ function tabHeadline(tab: BrainTab) {
489
+ if (tab === "memory") return "Memories, before mechanics.";
490
+ if (tab === "knowledge") return "Knowledge, gathered into recall.";
491
+ if (tab === "relationships") return "Relationships, when you need the why.";
492
+ if (tab === "graph") return "The graph, intentionally opened.";
493
+ if (tab === "portability") return "Care for the Brain.";
494
+ return "Talk to your Brain.";
495
+ }
496
+
476
497
  function GraphStatus({ data }: { data: Record<string, unknown> }) {
498
+ const mode = useAppStore((state) => state.mode);
477
499
  const nodeTypes = Object.keys((data.nodes as Record<string, unknown>) || {});
478
500
  const edgeTypes = Object.keys((data.edges as Record<string, unknown>) || {});
479
501
  return (
480
502
  <div className="space-y-3">
481
503
  <StatGrid stats={[
482
- { label: "Nodes", value: data.total_nodes ?? nodeTypes.reduce((sum, key) => sum + Number(((data.nodes as Record<string, unknown>) || {})[key] || 0), 0) },
483
- { label: "Edges", value: data.total_edges ?? edgeTypes.reduce((sum, key) => sum + Number(((data.edges as Record<string, unknown>) || {})[key] || 0), 0) },
484
- { label: "Node types", value: nodeTypes.length },
485
- { label: "Edge types", value: edgeTypes.length },
504
+ { label: "Memories", value: data.total_nodes ?? nodeTypes.reduce((sum, key) => sum + Number(((data.nodes as Record<string, unknown>) || {})[key] || 0), 0) },
505
+ { label: "Links", value: data.total_edges ?? edgeTypes.reduce((sum, key) => sum + Number(((data.edges as Record<string, unknown>) || {})[key] || 0), 0) },
506
+ { label: "Memory kinds", value: nodeTypes.length },
507
+ { label: "Link kinds", value: edgeTypes.length },
486
508
  ]} />
487
- <StructuredView value={{ node_types: nodeTypes, edge_types: edgeTypes }} />
509
+ {mode === "basic" ? (
510
+ <div className="flex flex-wrap gap-1">
511
+ {[...nodeTypes, ...edgeTypes].slice(0, 10).map((item) => <Badge key={item} variant="muted">{titleize(item)}</Badge>)}
512
+ </div>
513
+ ) : <StructuredView value={{ memory_kinds: nodeTypes, link_kinds: edgeTypes }} />}
488
514
  </div>
489
515
  );
490
516
  }
@@ -515,10 +541,11 @@ function MemoryStatus({ data }: { data: Record<string, unknown> }) {
515
541
  }
516
542
 
517
543
  function DigitalBrainExplorer({ data }: { data: unknown }) {
544
+ const mode = useAppStore((state) => state.mode);
518
545
  const parsed = React.useMemo(() => parseGraph(data), [data]);
519
546
  const [search, setSearch] = React.useState("");
520
547
  const [groupFilter, setGroupFilter] = React.useState("all");
521
- const [minImportance, setMinImportance] = React.useState(0);
548
+ const [minImportance, setMinImportance] = React.useState(mode === "basic" ? 0.1 : 0);
522
549
  const [labelMode, setLabelMode] = React.useState<LabelMode>("important");
523
550
  const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
524
551
  const [selectedId, setSelectedId] = React.useState<string | null>(null);
@@ -544,11 +571,14 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
544
571
  return next;
545
572
  });
546
573
  };
574
+ React.useEffect(() => {
575
+ if (mode === "basic" && minImportance < 0.1) setMinImportance(0.1);
576
+ }, [mode, minImportance]);
547
577
  if (!parsed.nodes.length) {
548
578
  return (
549
579
  <EmptyState
550
- title="No graph records yet"
551
- detail="Capture a document, note, or local folder to create graph nodes with provenance."
580
+ title="No relationship records yet"
581
+ detail="Capture a document, note, or local folder to create connected memories with sources."
552
582
  />
553
583
  );
554
584
  }
@@ -557,7 +587,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
557
587
  <div className="grid gap-3 xl:grid-cols-[1fr_220px_180px_170px]">
558
588
  <div className="relative">
559
589
  <Search className="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
560
- <Input className="pl-9" value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search graph labels, types, provenance..." />
590
+ <Input className="pl-9" value={search} onChange={(event) => setSearch(event.target.value)} placeholder={mode === "basic" ? "Search ideas, files, people, and notes..." : "Search graph labels, types, provenance..."} />
561
591
  </div>
562
592
  <select className="h-9 rounded-md border border-border bg-background px-3 text-sm" value={groupFilter} onChange={(event) => setGroupFilter(event.target.value)}>
563
593
  <option value="all">All semantic groups</option>
@@ -574,9 +604,9 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
574
604
  <Card>
575
605
  <CardHeader className="flex-row items-start justify-between gap-3">
576
606
  <div>
577
- <CardTitle className="flex items-center gap-2"><Layers3 className="h-4 w-4" /> Semantic map</CardTitle>
607
+ <CardTitle className="flex items-center gap-2"><Layers3 className="h-4 w-4" /> Deep graph</CardTitle>
578
608
  <CardDescription>
579
- Showing {fmtNumber(model.visibleNodes.length)} nodes and {fmtNumber(model.visibleEdges.length)} relationships from {fmtNumber(model.totalNodes)} graph nodes.
609
+ Showing {fmtNumber(model.visibleNodes.length)} ideas and {fmtNumber(model.visibleEdges.length)} connections from {fmtNumber(model.totalNodes)} saved items.
580
610
  </CardDescription>
581
611
  </div>
582
612
  <Badge variant={model.hiddenByFilters ? "warning" : "success"}>{model.hiddenByFilters ? `${fmtNumber(model.hiddenByFilters)} filtered` : "all in view"}</Badge>
@@ -594,11 +624,11 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
594
624
  value={minImportance}
595
625
  onChange={(event) => setMinImportance(Number(event.target.value))}
596
626
  className="w-44"
597
- aria-label="Minimum graph importance"
627
+ aria-label="Minimum relationship importance"
598
628
  />
599
629
  <Badge variant="muted">{Math.round(minImportance * 100)}%+</Badge>
600
630
  {selectedId ? <Button variant="outline" size="sm" onClick={() => setSelectedId(null)}>Clear focus</Button> : null}
601
- {search.trim() ? <Button variant="outline" size="sm" onClick={() => backendSearch.mutate()} disabled={backendSearch.isPending}>Search brain</Button> : null}
631
+ {search.trim() ? <Button variant="outline" size="sm" onClick={() => backendSearch.mutate()} disabled={backendSearch.isPending}>Search all memories</Button> : null}
602
632
  </div>
603
633
  <div className="flex flex-wrap gap-2">
604
634
  {model.groups.map((group) => (
@@ -620,7 +650,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
620
650
  <Card>
621
651
  <CardHeader>
622
652
  <CardTitle className="flex items-center gap-2"><Focus className="h-4 w-4" /> Focus</CardTitle>
623
- <CardDescription>Click a node to inspect its neighborhood.</CardDescription>
653
+ <CardDescription>Click any idea to see why it matters.</CardDescription>
624
654
  </CardHeader>
625
655
  <CardContent className="space-y-3">
626
656
  {selected ? (
@@ -634,11 +664,18 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
634
664
  </div>
635
665
  </div>
636
666
  {selected.summary ? <p className="text-sm text-muted-foreground">{selected.summary}</p> : null}
637
- <StructuredView value={{
638
- id: selected.id,
639
- degree: selected.degree,
640
- source: selected.source || "not reported",
641
- }} />
667
+ {mode === "basic" ? (
668
+ <KeyValueList data={{
669
+ connections: selected.degree,
670
+ source: selected.source || "not reported",
671
+ }} />
672
+ ) : (
673
+ <StructuredView value={{
674
+ id: selected.id,
675
+ degree: selected.degree,
676
+ source: selected.source || "not reported",
677
+ }} />
678
+ )}
642
679
  </>
643
680
  ) : selectedGroup ? (
644
681
  <div className="space-y-2">
@@ -646,13 +683,13 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
646
683
  <div className="text-lg font-semibold">{selectedGroup.label}</div>
647
684
  <Button variant="outline" onClick={() => toggleGroup(selectedGroup.id)}>Expand group</Button>
648
685
  </div>
649
- ) : <EmptyState title="No node selected" detail="Select a node or collapsed group in the graph." />}
686
+ ) : <EmptyState title="Nothing selected" detail="Select an item or collapsed group in the graph." />}
650
687
  </CardContent>
651
688
  </Card>
652
689
  <Card>
653
690
  <CardHeader>
654
- <CardTitle>Important nodes</CardTitle>
655
- <CardDescription>Highest-ranked visible graph records.</CardDescription>
691
+ <CardTitle>Important ideas</CardTitle>
692
+ <CardDescription>What Lattice thinks is most connected right now.</CardDescription>
656
693
  </CardHeader>
657
694
  <CardContent className="space-y-2">
658
695
  {model.visibleNodes.slice(0, 8).map((node) => (
@@ -687,12 +724,12 @@ function HybridSearch() {
687
724
  return (
688
725
  <Card>
689
726
  <CardHeader>
690
- <CardTitle className="flex items-center gap-2"><Search className="h-4 w-4" /> Hybrid search</CardTitle>
691
- <CardDescription>Calls the backend fused search endpoint and renders records with returned source scores.</CardDescription>
727
+ <CardTitle className="flex items-center gap-2"><Search className="h-4 w-4" /> Brain search</CardTitle>
728
+ <CardDescription>Find ideas across memories, documents, and connections.</CardDescription>
692
729
  </CardHeader>
693
730
  <CardContent className="space-y-3">
694
731
  <div className="flex flex-col gap-2 sm:flex-row">
695
- <Input placeholder="Search memories, graph nodes, and indexed documents" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && search.mutate()} />
732
+ <Input placeholder="Search memories, indexed documents, and relationships" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && search.mutate()} />
696
733
  <Button onClick={() => search.mutate()} disabled={!query.trim() || search.isPending}>Search</Button>
697
734
  </div>
698
735
  {search.data ? (
@@ -717,7 +754,7 @@ function MemoryPanel() {
717
754
  <Card>
718
755
  <CardHeader>
719
756
  <CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Recall</CardTitle>
720
- <CardDescription>Searches the real memory recall endpoint.</CardDescription>
757
+ <CardDescription>Bring back related memories from your workspace.</CardDescription>
721
758
  </CardHeader>
722
759
  <CardContent className="space-y-3">
723
760
  <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Recall memories about..." />
@@ -741,7 +778,7 @@ function ProvenancePanel() {
741
778
  <DataPanel title="Coverage" result={coverage.data}>
742
779
  {(data) => <StructuredView value={data} />}
743
780
  </DataPanel>
744
- <DataPanel title="Recent ingestion provenance" result={provenance.data}>
781
+ <DataPanel title="Recent sources" result={provenance.data}>
745
782
  {(data) => <EntityList items={(data as Record<string, unknown>).items || data} titleKey="source" metaKey="source_type" limit={14} />}
746
783
  </DataPanel>
747
784
  </div>
@@ -770,22 +807,22 @@ function PortabilityPanel() {
770
807
  <Card>
771
808
  <CardHeader>
772
809
  <CardTitle className="flex items-center gap-2"><DatabaseBackup className="h-4 w-4" /> Export, backup, import</CardTitle>
773
- <CardDescription>Every control calls a real portability endpoint. Import is dry-run by default from a pasted export artifact.</CardDescription>
810
+ <CardDescription>Export, back up, or preview an import before changing your brain.</CardDescription>
774
811
  </CardHeader>
775
812
  <CardContent className="space-y-3">
776
813
  <div className="flex flex-wrap gap-2">
777
- <ActionButton label="Export graph artifact" action={() => latticeApi.graphExport()} />
814
+ <ActionButton label="Export Brain" action={() => latticeApi.graphExport()} />
778
815
  <ActionButton label="Create backup" action={() => latticeApi.graphBackup()} />
779
816
  </div>
780
- <Textarea value={artifact} onChange={(e) => setArtifact(e.target.value)} placeholder="Paste an exported graph artifact for dry-run import" />
817
+ <Textarea value={artifact} onChange={(e) => setArtifact(e.target.value)} placeholder="Paste an exported Brain artifact to preview import" />
781
818
  <Button
782
819
  variant="outline"
783
820
  disabled={!artifact.trim() || importMutation.isPending}
784
821
  onClick={() => importMutation.mutate()}
785
822
  >
786
- Dry-run import
823
+ Preview import
787
824
  </Button>
788
- {importMutation.data ? <OperationResult result={importMutation.data} successLabel="Dry run completed" /> : null}
825
+ {importMutation.data ? <OperationResult result={importMutation.data} successLabel="Import preview completed" /> : null}
789
826
  </CardContent>
790
827
  </Card>
791
828
  </div>
@@ -798,9 +835,9 @@ function PortabilityStatus({ data }: { data: Record<string, unknown> }) {
798
835
  return (
799
836
  <div className="space-y-3">
800
837
  <StatGrid stats={[
801
- { label: "Schema", value: data.graph_schema_version || data.schema_version || "reported" },
802
- { label: "Nodes", value: (stats.total_nodes as number) || Object.values((stats.nodes as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
803
- { label: "Edges", value: (stats.total_edges as number) || Object.values((stats.edges as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
838
+ { label: "Brain format", value: data.graph_schema_version || data.schema_version || "reported" },
839
+ { label: "Memories", value: (stats.total_nodes as number) || Object.values((stats.nodes as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
840
+ { label: "Links", value: (stats.total_edges as number) || Object.values((stats.edges as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
804
841
  { label: "Storage", value: storage.engine || "reported" },
805
842
  ]} />
806
843
  <StructuredView value={data} />