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.
- package/README.md +77 -33
- package/docs/CHANGELOG.md +128 -0
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
- package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
- package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
- package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
- package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
- package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
- package/docs/V4_5_1_UX_REPORT.md +45 -0
- package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -17
- package/docs/architecture.md +8 -4
- package/frontend/index.html +2 -2
- package/frontend/src/App.tsx +120 -98
- package/frontend/src/api/client.ts +84 -1
- package/frontend/src/components/BrainConversation.tsx +301 -0
- package/frontend/src/components/FirstRunGuide.tsx +99 -0
- package/frontend/src/components/LivingBrain.tsx +121 -0
- package/frontend/src/components/ProductFlow.tsx +596 -0
- package/frontend/src/components/primitives.tsx +131 -25
- package/frontend/src/components/ui/badge.tsx +2 -2
- package/frontend/src/components/ui/button.tsx +7 -7
- package/frontend/src/components/ui/card.tsx +5 -5
- package/frontend/src/components/ui/input.tsx +1 -1
- package/frontend/src/components/ui/textarea.tsx +1 -1
- package/frontend/src/pages/Act.tsx +58 -28
- package/frontend/src/pages/Ask.tsx +2 -197
- package/frontend/src/pages/Brain.tsx +108 -71
- package/frontend/src/pages/Capture.tsx +24 -24
- package/frontend/src/pages/Library.tsx +222 -32
- package/frontend/src/pages/System.tsx +56 -34
- package/frontend/src/routes.ts +16 -25
- package/frontend/src/store/appStore.ts +8 -1
- package/frontend/src/styles.css +1663 -36
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +107 -18
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/model_compat.py +250 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +136 -32
- package/latticeai/services/model_catalog.py +2 -2
- package/latticeai/services/model_recommendation.py +8 -1
- package/latticeai/services/model_runtime.py +18 -3
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +12 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-By-G-Kay.css +2 -0
- package/static/app/assets/index-CJx6WuQH.js +336 -0
- package/static/app/assets/index-CJx6WuQH.js.map +1 -0
- package/static/app/index.html +4 -4
- package/static/manifest.json +1 -1
- package/static/app/assets/index-CHHal8Zl.css +0 -2
- package/static/app/assets/index-pdzil9ac.js +0 -333
- package/static/app/assets/index-pdzil9ac.js.map +0 -1
|
@@ -1,200 +1,5 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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 {
|
|
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 = "
|
|
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: "
|
|
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: "
|
|
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-
|
|
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
|
|
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
|
-
|
|
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-
|
|
427
|
-
|
|
428
|
-
<
|
|
429
|
-
<div
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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 === "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
462
|
-
<DataPanel title="
|
|
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: "
|
|
483
|
-
{ label: "
|
|
484
|
-
{ label: "
|
|
485
|
-
{ label: "
|
|
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
|
-
|
|
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
|
|
551
|
-
detail="Capture a document, note, or local folder to create
|
|
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" />
|
|
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)}
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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="
|
|
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
|
|
655
|
-
<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" />
|
|
691
|
-
<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,
|
|
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>
|
|
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
|
|
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>
|
|
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
|
|
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
|
|
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
|
-
|
|
823
|
+
Preview import
|
|
787
824
|
</Button>
|
|
788
|
-
{importMutation.data ? <OperationResult result={importMutation.data} successLabel="
|
|
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: "
|
|
802
|
-
{ label: "
|
|
803
|
-
{ label: "
|
|
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} />
|