ltcai 4.5.1 → 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 +50 -34
- package/docs/CHANGELOG.md +43 -0
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +58 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -15
- package/frontend/index.html +2 -2
- package/frontend/src/App.tsx +18 -57
- package/frontend/src/api/client.ts +1 -0
- package/frontend/src/components/BrainConversation.tsx +301 -0
- package/frontend/src/components/FirstRunGuide.tsx +4 -4
- package/frontend/src/components/LivingBrain.tsx +121 -0
- package/frontend/src/components/ProductFlow.tsx +596 -0
- package/frontend/src/pages/Ask.tsx +2 -229
- package/frontend/src/pages/Brain.tsx +68 -49
- package/frontend/src/routes.ts +15 -26
- package/frontend/src/styles.css +1065 -68
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -2
- 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-3G8qcrIS.js +0 -336
- package/static/app/assets/index-3G8qcrIS.js.map +0 -1
- package/static/app/assets/index-C0wYZp7k.css +0 -2
|
@@ -1,232 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { ImagePlus, MessageSquare, Send, Sparkles, 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 { useAppStore } from "@/store/appStore";
|
|
11
|
-
import { asArray } from "@/lib/utils";
|
|
12
|
-
|
|
13
|
-
type Msg = { role?: string; content?: string; timestamp?: string };
|
|
14
|
-
|
|
15
|
-
function fileToDataUrl(file: File) {
|
|
16
|
-
return new Promise<string>((resolve, reject) => {
|
|
17
|
-
const reader = new FileReader();
|
|
18
|
-
reader.onload = () => resolve(String(reader.result || ""));
|
|
19
|
-
reader.onerror = () => reject(reader.error);
|
|
20
|
-
reader.readAsDataURL(file);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
1
|
+
import { BrainConversation } from "@/components/BrainConversation";
|
|
23
2
|
|
|
24
3
|
export function AskPage() {
|
|
25
|
-
|
|
26
|
-
const history = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
|
|
27
|
-
const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
28
|
-
const [conversationId, setConversationId] = React.useState<string | null>(null);
|
|
29
|
-
const conversation = useQuery({
|
|
30
|
-
queryKey: ["conversation", conversationId],
|
|
31
|
-
queryFn: () => latticeApi.conversation(conversationId || ""),
|
|
32
|
-
enabled: !!conversationId,
|
|
33
|
-
});
|
|
34
|
-
const [messages, setMessages] = React.useState<Msg[]>([]);
|
|
35
|
-
const [draft, setDraft] = React.useState("");
|
|
36
|
-
const [imageData, setImageData] = React.useState<string | null>(null);
|
|
37
|
-
const [trace, setTrace] = React.useState<unknown>(null);
|
|
38
|
-
const [streaming, setStreaming] = React.useState(false);
|
|
39
|
-
|
|
40
|
-
React.useEffect(() => {
|
|
41
|
-
if (conversation.data?.ok) setMessages(asArray<Msg>((conversation.data.data as Record<string, unknown>).messages || conversation.data.data));
|
|
42
|
-
}, [conversation.data]);
|
|
43
|
-
|
|
44
|
-
const send = async () => {
|
|
45
|
-
const message = draft.trim();
|
|
46
|
-
if (!message || streaming) return;
|
|
47
|
-
setDraft("");
|
|
48
|
-
setMessages((items) => [...items, { role: "user", content: message }, { role: "assistant", content: "" }]);
|
|
49
|
-
setStreaming(true);
|
|
50
|
-
try {
|
|
51
|
-
const result = await latticeApi.streamChat(
|
|
52
|
-
{ message, conversation_id: conversationId || undefined, image_data: imageData || undefined },
|
|
53
|
-
{
|
|
54
|
-
onChunk: (_delta, fullText) => {
|
|
55
|
-
setMessages((items) => {
|
|
56
|
-
const next = [...items];
|
|
57
|
-
const last = next[next.length - 1] || { role: "assistant" };
|
|
58
|
-
next[next.length - 1] = { ...last, role: "assistant", content: fullText };
|
|
59
|
-
return next;
|
|
60
|
-
});
|
|
61
|
-
},
|
|
62
|
-
onTrace: setTrace,
|
|
63
|
-
},
|
|
64
|
-
);
|
|
65
|
-
if (result.error) {
|
|
66
|
-
setMessages((items) => {
|
|
67
|
-
const next = [...items];
|
|
68
|
-
next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
|
|
69
|
-
return next;
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
} finally {
|
|
73
|
-
setStreaming(false);
|
|
74
|
-
setImageData(null);
|
|
75
|
-
await qc.invalidateQueries({ queryKey: ["chatHistory"] });
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const deleteMutation = useMutation({
|
|
80
|
-
mutationFn: (id: string) => latticeApi.deleteConversation(id),
|
|
81
|
-
onSuccess: () => qc.invalidateQueries({ queryKey: ["chatHistory"] }),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<div className="space-y-5">
|
|
86
|
-
<header className="page-hero">
|
|
87
|
-
<div className="page-kicker"><MessageSquare className="h-4 w-4" /> Ask</div>
|
|
88
|
-
<h1 className="page-title">Think out loud with Lattice.</h1>
|
|
89
|
-
<p className="page-copy">Ask in plain language, attach an image, and let your private memory shape the answer.</p>
|
|
90
|
-
</header>
|
|
91
|
-
<div className="grid min-h-[calc(100vh-13rem)] gap-4 xl:grid-cols-[18rem_minmax(0,1fr)_22rem]">
|
|
92
|
-
<Card className="overflow-hidden">
|
|
93
|
-
<CardHeader>
|
|
94
|
-
<CardTitle className="flex items-center gap-2"><MessageSquare className="h-4 w-4" /> Conversations</CardTitle>
|
|
95
|
-
<CardDescription>Pick up where you left off.</CardDescription>
|
|
96
|
-
</CardHeader>
|
|
97
|
-
<CardContent className="soft-scrollbar max-h-[42rem] space-y-2 overflow-auto">
|
|
98
|
-
<SourceBadge result={history.data} />
|
|
99
|
-
{asArray<Record<string, unknown>>(history.data?.data).length ? asArray<Record<string, unknown>>(history.data?.data).map((item) => (
|
|
100
|
-
<button
|
|
101
|
-
key={String(item.id)}
|
|
102
|
-
onClick={() => setConversationId(String(item.id))}
|
|
103
|
-
className="block w-full rounded-lg border border-border bg-background/55 p-3 text-left text-sm transition hover:bg-muted"
|
|
104
|
-
>
|
|
105
|
-
<div className="font-medium">{String(item.title || item.id)}</div>
|
|
106
|
-
<div className="mt-1 flex items-center justify-between gap-2 text-xs text-muted-foreground">
|
|
107
|
-
<span>{String(item.updated_at || "")}</span>
|
|
108
|
-
<Trash2
|
|
109
|
-
className="h-3.5 w-3.5"
|
|
110
|
-
onClick={(e) => {
|
|
111
|
-
e.stopPropagation();
|
|
112
|
-
deleteMutation.mutate(String(item.id));
|
|
113
|
-
}}
|
|
114
|
-
/>
|
|
115
|
-
</div>
|
|
116
|
-
</button>
|
|
117
|
-
)) : <EmptyState title="No conversations" detail="Start a new exchange or sign in to load history." />}
|
|
118
|
-
</CardContent>
|
|
119
|
-
</Card>
|
|
120
|
-
|
|
121
|
-
<section className="premium-surface flex min-h-[42rem] flex-col overflow-hidden rounded-lg">
|
|
122
|
-
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-5">
|
|
123
|
-
<div>
|
|
124
|
-
<h2 className="text-xl font-semibold">New conversation</h2>
|
|
125
|
-
<p className="text-sm text-muted-foreground">Lattice answers only when a real model is available.</p>
|
|
126
|
-
</div>
|
|
127
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
128
|
-
<Badge variant="muted">{String((models.data?.data as Record<string, unknown>)?.current || "no model loaded")}</Badge>
|
|
129
|
-
<SourceBadge result={models.data} />
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
<div className="soft-scrollbar flex-1 space-y-4 overflow-auto p-5">
|
|
133
|
-
{messages.length ? messages.map((msg, index) => (
|
|
134
|
-
<div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
|
135
|
-
<div className={`max-w-[78%] rounded-lg border p-4 text-sm leading-6 ${msg.role === "user" ? "border-primary/30 bg-primary/15" : "border-border bg-background/68"}`}>
|
|
136
|
-
<div className="mb-1 text-xs uppercase text-muted-foreground">{msg.role || "message"}</div>
|
|
137
|
-
<div className="whitespace-pre-wrap">{msg.content}</div>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
)) : (
|
|
141
|
-
<div className="grid min-h-full place-items-center">
|
|
142
|
-
<EmptyState
|
|
143
|
-
title="What should we think through?"
|
|
144
|
-
detail="Ask about a document, a project, a memory, or a question you want Lattice to connect across your workspace."
|
|
145
|
-
/>
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
<div className="border-t border-border bg-background/28 p-5">
|
|
150
|
-
{imageData ? <Badge variant="success" className="mb-2">image attached</Badge> : null}
|
|
151
|
-
<Textarea
|
|
152
|
-
value={draft}
|
|
153
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
154
|
-
onKeyDown={(e) => {
|
|
155
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
156
|
-
e.preventDefault();
|
|
157
|
-
void send();
|
|
158
|
-
}
|
|
159
|
-
}}
|
|
160
|
-
placeholder="Ask anything about your work..."
|
|
161
|
-
/>
|
|
162
|
-
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
|
163
|
-
<label className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted">
|
|
164
|
-
<ImagePlus className="h-4 w-4" />
|
|
165
|
-
Image
|
|
166
|
-
<input
|
|
167
|
-
type="file"
|
|
168
|
-
accept="image/*"
|
|
169
|
-
className="sr-only"
|
|
170
|
-
onChange={async (e) => {
|
|
171
|
-
const file = e.target.files?.[0];
|
|
172
|
-
if (file) setImageData(await fileToDataUrl(file));
|
|
173
|
-
}}
|
|
174
|
-
/>
|
|
175
|
-
</label>
|
|
176
|
-
<Button disabled={!draft.trim() || streaming} onClick={() => void send()}>
|
|
177
|
-
<Send className="h-4 w-4" /> Send
|
|
178
|
-
</Button>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
</section>
|
|
182
|
-
|
|
183
|
-
<aside className="space-y-4">
|
|
184
|
-
<ContextPreview question={draft || [...messages].reverse().find((m: Msg) => m.role === "user")?.content || ""} trace={trace} />
|
|
185
|
-
</aside>
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function ContextPreview({ question, trace }: { question: string; trace: unknown }) {
|
|
192
|
-
const mode = useAppStore((state) => state.mode);
|
|
193
|
-
const hybrid = useQuery({
|
|
194
|
-
queryKey: ["askHybrid", question],
|
|
195
|
-
queryFn: () => latticeApi.hybridSearch(question),
|
|
196
|
-
enabled: question.trim().length > 2,
|
|
197
|
-
});
|
|
198
|
-
const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
|
|
199
|
-
if (mode === "basic") {
|
|
200
|
-
return (
|
|
201
|
-
<>
|
|
202
|
-
<DataPanel title="Relevant memories" result={hybrid.data}>
|
|
203
|
-
{(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={5} />}
|
|
204
|
-
</DataPanel>
|
|
205
|
-
<Card>
|
|
206
|
-
<CardHeader>
|
|
207
|
-
<CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Sources</CardTitle>
|
|
208
|
-
<CardDescription>Lattice shows supporting memories when an answer uses them.</CardDescription>
|
|
209
|
-
</CardHeader>
|
|
210
|
-
<CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="Ask to see sources" />}</CardContent>
|
|
211
|
-
</Card>
|
|
212
|
-
</>
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return (
|
|
216
|
-
<>
|
|
217
|
-
<DataPanel title="Memory preview" result={hybrid.data}>
|
|
218
|
-
{(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={5} />}
|
|
219
|
-
</DataPanel>
|
|
220
|
-
<DataPanel title="Graph context" result={graph.data}>
|
|
221
|
-
{(data) => <EntityList items={(data as Record<string, unknown>).nodes} titleKey="title" metaKey="type" limit={5} />}
|
|
222
|
-
</DataPanel>
|
|
223
|
-
<Card>
|
|
224
|
-
<CardHeader>
|
|
225
|
-
<CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Why this context</CardTitle>
|
|
226
|
-
<CardDescription>Signals Lattice used to choose supporting memories.</CardDescription>
|
|
227
|
-
</CardHeader>
|
|
228
|
-
<CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="Ask to see context" />}</CardContent>
|
|
229
|
-
</Card>
|
|
230
|
-
</>
|
|
231
|
-
);
|
|
4
|
+
return <BrainConversation />;
|
|
232
5
|
}
|
|
@@ -3,6 +3,7 @@ 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 { BrainConversation } from "@/components/BrainConversation";
|
|
6
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";
|
|
@@ -12,7 +13,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|
|
12
13
|
import { useAppStore } from "@/store/appStore";
|
|
13
14
|
import { asArray, fmtNumber, pct, shortId, titleize } from "@/lib/utils";
|
|
14
15
|
|
|
15
|
-
type BrainTab = "
|
|
16
|
+
type BrainTab = "conversation" | "memory" | "knowledge" | "relationships" | "graph" | "portability";
|
|
16
17
|
type LabelMode = "important" | "all" | "off";
|
|
17
18
|
|
|
18
19
|
type GraphNode = {
|
|
@@ -61,12 +62,12 @@ type ExplorerModel = ParsedGraph & {
|
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
const tabs: Array<{ id: BrainTab; label: string }> = [
|
|
64
|
-
{ id: "
|
|
65
|
-
{ id: "
|
|
66
|
-
{ id: "
|
|
67
|
-
{ id: "
|
|
68
|
-
{ id: "
|
|
69
|
-
{ id: "portability", label: "
|
|
65
|
+
{ id: "conversation", label: "Brain" },
|
|
66
|
+
{ id: "memory", label: "Memories" },
|
|
67
|
+
{ id: "knowledge", label: "Knowledge" },
|
|
68
|
+
{ id: "relationships", label: "Relationships" },
|
|
69
|
+
{ id: "graph", label: "Graph" },
|
|
70
|
+
{ id: "portability", label: "Care" },
|
|
70
71
|
];
|
|
71
72
|
|
|
72
73
|
const groupDefinitions = [
|
|
@@ -413,9 +414,10 @@ function CytoscapeGraph({
|
|
|
413
414
|
|
|
414
415
|
export function BrainPage({ initialTab }: { initialTab?: string }) {
|
|
415
416
|
const mode = useAppStore((state) => state.mode);
|
|
416
|
-
const
|
|
417
|
+
const normalizedInitialTab = normalizeBrainTab(initialTab);
|
|
418
|
+
const [tab, setTab] = React.useState<BrainTab>(normalizedInitialTab);
|
|
417
419
|
React.useEffect(() => {
|
|
418
|
-
|
|
420
|
+
setTab(normalizeBrainTab(initialTab));
|
|
419
421
|
}, [initialTab]);
|
|
420
422
|
const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
|
|
421
423
|
const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
|
|
@@ -426,31 +428,32 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
|
|
|
426
428
|
|
|
427
429
|
return (
|
|
428
430
|
<div className="space-y-5">
|
|
429
|
-
|
|
430
|
-
<
|
|
431
|
-
<div
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
</div>
|
|
442
|
-
</header>
|
|
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
|
+
)}
|
|
443
443
|
<Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as BrainTab)} />
|
|
444
444
|
|
|
445
|
-
{tab === "
|
|
445
|
+
{tab === "conversation" ? <BrainConversation /> : null}
|
|
446
|
+
{tab === "memory" ? <MemoryPanel /> : null}
|
|
447
|
+
{tab === "knowledge" ? <HybridSearch /> : null}
|
|
448
|
+
{tab === "relationships" ? (
|
|
446
449
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
447
|
-
<DataPanel title="Brain
|
|
450
|
+
<DataPanel title="Brain activity" result={stats.data}>
|
|
448
451
|
{(data) => <GraphStatus data={data as Record<string, unknown>} />}
|
|
449
452
|
</DataPanel>
|
|
450
|
-
<DataPanel title="Retrieval
|
|
453
|
+
<DataPanel title="Retrieval rhythm" result={index.data}>
|
|
451
454
|
{(data) => <RetrievalStatus data={data as Record<string, unknown>} />}
|
|
452
455
|
</DataPanel>
|
|
453
|
-
<DataPanel title="Memory
|
|
456
|
+
<DataPanel title="Memory layers" result={memory.data}>
|
|
454
457
|
{(data) => <MemoryStatus data={data as Record<string, unknown>} />}
|
|
455
458
|
</DataPanel>
|
|
456
459
|
<DataPanel title="Recent sources" result={provenance.data}>
|
|
@@ -460,21 +463,37 @@ export function BrainPage({ initialTab }: { initialTab?: string }) {
|
|
|
460
463
|
) : null}
|
|
461
464
|
|
|
462
465
|
{tab === "graph" ? (
|
|
463
|
-
graph.isLoading ? <LoadingPanel title="
|
|
464
|
-
<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}>
|
|
465
468
|
{(data) => <DigitalBrainExplorer data={data} />}
|
|
466
469
|
</DataPanel>
|
|
467
470
|
)
|
|
468
471
|
) : null}
|
|
469
|
-
|
|
470
|
-
{tab === "search" ? <HybridSearch /> : null}
|
|
471
|
-
{tab === "memory" ? <MemoryPanel /> : null}
|
|
472
|
-
{tab === "provenance" ? <ProvenancePanel /> : null}
|
|
473
472
|
{tab === "portability" ? <PortabilityPanel /> : null}
|
|
474
473
|
</div>
|
|
475
474
|
);
|
|
476
475
|
}
|
|
477
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
|
+
|
|
478
497
|
function GraphStatus({ data }: { data: Record<string, unknown> }) {
|
|
479
498
|
const mode = useAppStore((state) => state.mode);
|
|
480
499
|
const nodeTypes = Object.keys((data.nodes as Record<string, unknown>) || {});
|
|
@@ -482,16 +501,16 @@ function GraphStatus({ data }: { data: Record<string, unknown> }) {
|
|
|
482
501
|
return (
|
|
483
502
|
<div className="space-y-3">
|
|
484
503
|
<StatGrid stats={[
|
|
485
|
-
{ label: "
|
|
486
|
-
{ label: "
|
|
487
|
-
{ label: "
|
|
488
|
-
{ 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 },
|
|
489
508
|
]} />
|
|
490
509
|
{mode === "basic" ? (
|
|
491
510
|
<div className="flex flex-wrap gap-1">
|
|
492
511
|
{[...nodeTypes, ...edgeTypes].slice(0, 10).map((item) => <Badge key={item} variant="muted">{titleize(item)}</Badge>)}
|
|
493
512
|
</div>
|
|
494
|
-
) : <StructuredView value={{
|
|
513
|
+
) : <StructuredView value={{ memory_kinds: nodeTypes, link_kinds: edgeTypes }} />}
|
|
495
514
|
</div>
|
|
496
515
|
);
|
|
497
516
|
}
|
|
@@ -558,8 +577,8 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
|
|
|
558
577
|
if (!parsed.nodes.length) {
|
|
559
578
|
return (
|
|
560
579
|
<EmptyState
|
|
561
|
-
title="No
|
|
562
|
-
detail="Capture a document, note, or local folder to create connected
|
|
580
|
+
title="No relationship records yet"
|
|
581
|
+
detail="Capture a document, note, or local folder to create connected memories with sources."
|
|
563
582
|
/>
|
|
564
583
|
);
|
|
565
584
|
}
|
|
@@ -585,7 +604,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
|
|
|
585
604
|
<Card>
|
|
586
605
|
<CardHeader className="flex-row items-start justify-between gap-3">
|
|
587
606
|
<div>
|
|
588
|
-
<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>
|
|
589
608
|
<CardDescription>
|
|
590
609
|
Showing {fmtNumber(model.visibleNodes.length)} ideas and {fmtNumber(model.visibleEdges.length)} connections from {fmtNumber(model.totalNodes)} saved items.
|
|
591
610
|
</CardDescription>
|
|
@@ -605,7 +624,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
|
|
|
605
624
|
value={minImportance}
|
|
606
625
|
onChange={(event) => setMinImportance(Number(event.target.value))}
|
|
607
626
|
className="w-44"
|
|
608
|
-
aria-label="Minimum
|
|
627
|
+
aria-label="Minimum relationship importance"
|
|
609
628
|
/>
|
|
610
629
|
<Badge variant="muted">{Math.round(minImportance * 100)}%+</Badge>
|
|
611
630
|
{selectedId ? <Button variant="outline" size="sm" onClick={() => setSelectedId(null)}>Clear focus</Button> : null}
|
|
@@ -664,7 +683,7 @@ function DigitalBrainExplorer({ data }: { data: unknown }) {
|
|
|
664
683
|
<div className="text-lg font-semibold">{selectedGroup.label}</div>
|
|
665
684
|
<Button variant="outline" onClick={() => toggleGroup(selectedGroup.id)}>Expand group</Button>
|
|
666
685
|
</div>
|
|
667
|
-
) : <EmptyState title="
|
|
686
|
+
) : <EmptyState title="Nothing selected" detail="Select an item or collapsed group in the graph." />}
|
|
668
687
|
</CardContent>
|
|
669
688
|
</Card>
|
|
670
689
|
<Card>
|
|
@@ -710,7 +729,7 @@ function HybridSearch() {
|
|
|
710
729
|
</CardHeader>
|
|
711
730
|
<CardContent className="space-y-3">
|
|
712
731
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
713
|
-
<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()} />
|
|
714
733
|
<Button onClick={() => search.mutate()} disabled={!query.trim() || search.isPending}>Search</Button>
|
|
715
734
|
</div>
|
|
716
735
|
{search.data ? (
|
|
@@ -792,10 +811,10 @@ function PortabilityPanel() {
|
|
|
792
811
|
</CardHeader>
|
|
793
812
|
<CardContent className="space-y-3">
|
|
794
813
|
<div className="flex flex-wrap gap-2">
|
|
795
|
-
<ActionButton label="Export
|
|
814
|
+
<ActionButton label="Export Brain" action={() => latticeApi.graphExport()} />
|
|
796
815
|
<ActionButton label="Create backup" action={() => latticeApi.graphBackup()} />
|
|
797
816
|
</div>
|
|
798
|
-
<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" />
|
|
799
818
|
<Button
|
|
800
819
|
variant="outline"
|
|
801
820
|
disabled={!artifact.trim() || importMutation.isPending}
|
|
@@ -816,9 +835,9 @@ function PortabilityStatus({ data }: { data: Record<string, unknown> }) {
|
|
|
816
835
|
return (
|
|
817
836
|
<div className="space-y-3">
|
|
818
837
|
<StatGrid stats={[
|
|
819
|
-
{ label: "
|
|
820
|
-
{ label: "
|
|
821
|
-
{ 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) },
|
|
822
841
|
{ label: "Storage", value: storage.engine || "reported" },
|
|
823
842
|
]} />
|
|
824
843
|
<StructuredView value={data} />
|
package/frontend/src/routes.ts
CHANGED
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Activity,
|
|
3
2
|
Brain,
|
|
4
3
|
Database,
|
|
5
4
|
FolderInput,
|
|
6
5
|
Library,
|
|
7
|
-
MessageSquare,
|
|
8
|
-
Network,
|
|
9
6
|
Settings,
|
|
10
|
-
Shield,
|
|
11
7
|
Workflow,
|
|
12
|
-
Zap,
|
|
13
8
|
} from "lucide-react";
|
|
14
9
|
|
|
15
|
-
export type PrimaryRoute = "brain" | "
|
|
10
|
+
export type PrimaryRoute = "brain" | "memory" | "capture" | "act" | "library" | "system";
|
|
16
11
|
|
|
17
12
|
export const primaryRoutes = [
|
|
18
|
-
{ id: "brain", label: "
|
|
19
|
-
{ id: "
|
|
20
|
-
{ id: "capture", label: "
|
|
21
|
-
{ id: "act", label: "
|
|
22
|
-
{ id: "library", label: "
|
|
23
|
-
{ id: "system", label: "
|
|
13
|
+
{ id: "brain", label: "Brain", icon: Brain, description: "Talk with your living Brain" },
|
|
14
|
+
{ id: "memory", label: "Memory", icon: Database, description: "Recall what your Brain remembers" },
|
|
15
|
+
{ id: "capture", label: "Files", icon: FolderInput, description: "Bring in files, folders, and pages" },
|
|
16
|
+
{ id: "act", label: "Automations", icon: Workflow, description: "Turn goals into supervised runs" },
|
|
17
|
+
{ id: "library", label: "Models", icon: Library, description: "Choose the local model powering your Brain" },
|
|
18
|
+
{ id: "system", label: "Settings", icon: Settings, description: "Keep your Brain safe and portable" },
|
|
24
19
|
] as const;
|
|
25
20
|
|
|
26
21
|
export const routeAliases: Record<string, { primary: PrimaryRoute; tab?: string }> = {
|
|
27
|
-
home: { primary: "brain", tab: "
|
|
22
|
+
home: { primary: "brain", tab: "conversation" },
|
|
28
23
|
onboarding: { primary: "system", tab: "account" },
|
|
29
24
|
"knowledge-graph": { primary: "brain", tab: "graph" },
|
|
30
|
-
"hybrid-search": { primary: "brain", tab: "
|
|
31
|
-
memory: { primary: "
|
|
32
|
-
|
|
25
|
+
"hybrid-search": { primary: "brain", tab: "knowledge" },
|
|
26
|
+
memory: { primary: "memory", tab: "memory" },
|
|
27
|
+
ask: { primary: "brain", tab: "conversation" },
|
|
28
|
+
chat: { primary: "brain", tab: "conversation" },
|
|
33
29
|
files: { primary: "capture", tab: "files" },
|
|
34
30
|
pipeline: { primary: "capture", tab: "pipeline" },
|
|
35
31
|
"my-computer": { primary: "capture", tab: "local" },
|
|
@@ -58,19 +54,12 @@ export const routeAliases: Record<string, { primary: PrimaryRoute; tab?: string
|
|
|
58
54
|
};
|
|
59
55
|
|
|
60
56
|
export const commandRoutes = [
|
|
61
|
-
{ key: "brain", label: "
|
|
62
|
-
{ key: "onboarding", label: "First 10 Minutes", icon: Settings },
|
|
63
|
-
{ key: "knowledge-graph", label: "Memory Map", icon: Network },
|
|
64
|
-
{ key: "hybrid-search", label: "Search Everything", icon: Zap },
|
|
57
|
+
{ key: "brain", label: "Brain", icon: Brain },
|
|
65
58
|
{ key: "memory", label: "Memory", icon: Database },
|
|
66
|
-
{ key: "
|
|
67
|
-
{ key: "files", label: "Add Files", icon: FolderInput },
|
|
68
|
-
{ key: "agents", label: "Start a Run", icon: Workflow },
|
|
59
|
+
{ key: "files", label: "Files", icon: FolderInput },
|
|
69
60
|
{ key: "workflows", label: "Automations", icon: Workflow },
|
|
70
61
|
{ key: "models", label: "Models", icon: Library },
|
|
71
|
-
{ key: "
|
|
72
|
-
{ key: "activity", label: "Activity", icon: Activity },
|
|
73
|
-
{ key: "admin/security", label: "Security", icon: Shield },
|
|
62
|
+
{ key: "settings", label: "Settings", icon: Settings },
|
|
74
63
|
];
|
|
75
64
|
|
|
76
65
|
export function parseHash() {
|