ltcai 6.2.0 → 6.3.1

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 (51) hide show
  1. package/README.md +29 -17
  2. package/docs/CHANGELOG.md +66 -0
  3. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  4. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  5. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  6. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  7. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  8. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
  9. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  10. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +1 -1
  12. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  13. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  14. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  15. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  16. package/frontend/src/features/brain/BrainHome.tsx +28 -795
  17. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  18. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  19. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  20. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  21. package/frontend/src/features/brain/types.ts +6 -6
  22. package/frontend/src/features/review/ReviewCard.tsx +14 -10
  23. package/frontend/src/features/review/ReviewInbox.tsx +24 -9
  24. package/frontend/src/i18n.ts +208 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +139 -43
  27. package/frontend/src/pages/Library.tsx +72 -6
  28. package/lattice_brain/__init__.py +1 -1
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/app_factory.py +48 -106
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/access_runtime.py +97 -0
  35. package/latticeai/runtime/chat_wiring.py +119 -0
  36. package/latticeai/runtime/model_wiring.py +40 -0
  37. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  38. package/latticeai/runtime/review_wiring.py +37 -0
  39. package/latticeai/runtime/tail_wiring.py +21 -0
  40. package/package.json +2 -2
  41. package/scripts/check_i18n_literals.mjs +52 -0
  42. package/src-tauri/Cargo.lock +1 -1
  43. package/src-tauri/Cargo.toml +1 -1
  44. package/src-tauri/tauri.conf.json +1 -1
  45. package/static/app/asset-manifest.json +5 -5
  46. package/static/app/assets/index-Div5vMlq.css +2 -0
  47. package/static/app/assets/{index-D91Rz5--.js → index-t1jx1BR9.js} +2 -2
  48. package/static/app/assets/index-t1jx1BR9.js.map +1 -0
  49. package/static/app/index.html +2 -2
  50. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  51. package/static/app/assets/index-D91Rz5--.js.map +0 -1
@@ -1,33 +1,35 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { FolderPlus, Globe2, HardDrive, Upload } from "lucide-react";
3
+ import { AlertCircle, CheckCircle2, FolderPlus, Globe2, HardDrive, Loader2, RotateCcw, Upload } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
5
  import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
8
  import { Input } from "@/components/ui/input";
9
+ import { t, type Language } from "@/i18n";
9
10
  import { asArray } from "@/lib/utils";
11
+ import { useAppStore } from "@/store/appStore";
10
12
 
11
13
  type CaptureTab = "files" | "local" | "browser" | "pipeline";
12
14
 
13
- const tabs: Array<{ id: CaptureTab; label: string }> = [
14
- { id: "files", label: "Files" },
15
- { id: "local", label: "Folders" },
16
- { id: "browser", label: "Web" },
17
- { id: "pipeline", label: "Flow" },
18
- ];
19
-
20
15
  export function CapturePage({ initialTab }: { initialTab?: string }) {
16
+ const language = useAppStore((state) => state.language);
21
17
  const [tab, setTab] = React.useState<CaptureTab>((initialTab as CaptureTab) || "files");
18
+ const tabs: Array<{ id: CaptureTab; label: string }> = [
19
+ { id: "files", label: t(language, "capture.tab.files") },
20
+ { id: "local", label: t(language, "capture.tab.local") },
21
+ { id: "browser", label: t(language, "capture.tab.browser") },
22
+ { id: "pipeline", label: t(language, "capture.tab.pipeline") },
23
+ ];
22
24
  React.useEffect(() => {
23
25
  if (initialTab === "pipeline" || initialTab === "local" || initialTab === "files") setTab(initialTab);
24
26
  }, [initialTab]);
25
27
  return (
26
28
  <div className="space-y-5">
27
29
  <header className="page-hero">
28
- <div className="page-kicker"><Upload className="h-4 w-4" /> Add</div>
29
- <h1 className="page-title">Feed the brain what matters.</h1>
30
- <p className="page-copy">Drop in files, connect folders, or save a page. Lattice remembers the origin of every idea it learns.</p>
30
+ <div className="page-kicker"><Upload className="h-4 w-4" /> {t(language, "capture.kicker")}</div>
31
+ <h1 className="page-title">{t(language, "capture.title")}</h1>
32
+ <p className="page-copy">{t(language, "capture.body")}</p>
31
33
  </header>
32
34
  <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as CaptureTab)} />
33
35
  {tab === "files" ? <FilesPanel /> : null}
@@ -39,41 +41,133 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
39
41
  }
40
42
 
41
43
  function FilesPanel() {
44
+ const language = useAppStore((state) => state.language);
42
45
  const qc = useQueryClient();
43
46
  const docs = useQuery({ queryKey: ["documents"], queryFn: () => latticeApi.documents(200) });
47
+ const [queue, setQueue] = React.useState<UploadQueueItem[]>([]);
44
48
  const upload = useMutation({
45
- mutationFn: (files: FileList) => Promise.all(Array.from(files).map((file) => latticeApi.uploadDocument(file))),
46
- onSuccess: () => qc.invalidateQueries({ queryKey: ["documents"] }),
49
+ mutationFn: (files: File[]) => uploadFiles(files, setQueue),
50
+ onSuccess: () => {
51
+ void qc.invalidateQueries({ queryKey: ["documents"] });
52
+ void qc.invalidateQueries({ queryKey: ["graphStats"] });
53
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
54
+ },
47
55
  });
56
+ const beginUpload = React.useCallback((files: FileList | File[]) => {
57
+ const nextFiles = Array.from(files);
58
+ if (!nextFiles.length) return;
59
+ upload.mutate(nextFiles);
60
+ }, [upload]);
48
61
  return (
49
62
  <div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
50
63
  <Card>
51
64
  <CardHeader>
52
- <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> Add documents</CardTitle>
53
- <CardDescription>Choose files and Lattice will prepare them for search and memory.</CardDescription>
65
+ <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> {t(language, "capture.files.title")}</CardTitle>
66
+ <CardDescription>{t(language, "capture.files.description")}</CardDescription>
54
67
  </CardHeader>
55
68
  <CardContent>
56
- <label className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50">
69
+ <label
70
+ className="flex min-h-56 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-6 text-center transition hover:bg-muted/50"
71
+ onDragOver={(event) => event.preventDefault()}
72
+ onDrop={(event) => {
73
+ event.preventDefault();
74
+ beginUpload(event.dataTransfer.files);
75
+ }}
76
+ >
57
77
  <Upload className="h-7 w-7 text-primary" />
58
- <span className="text-lg font-semibold">Choose files</span>
59
- <span className="max-w-sm text-sm leading-6 text-muted-foreground">PDF, Office files, notes, markdown, text, and spreadsheets are all welcome.</span>
60
- <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
78
+ <span className="text-lg font-semibold">{t(language, "capture.files.drop")}</span>
79
+ <span className="max-w-sm text-sm leading-6 text-muted-foreground">{t(language, "capture.files.dropDetail")}</span>
80
+ <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && beginUpload(e.target.files)} />
61
81
  </label>
62
- {upload.data ? (
63
- <div className="mt-3 space-y-2">
64
- {upload.data.map((item, index) => <OperationResult key={index} result={item} successLabel="Upload completed" />)}
65
- </div>
66
- ) : null}
82
+ <DocumentUploadQueue queue={queue} onRetry={(file) => beginUpload([file])} />
67
83
  </CardContent>
68
84
  </Card>
69
- <DataPanel title="Uploaded documents" result={docs.data}>
70
- {(data) => <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />}
85
+ <DataPanel title={t(language, "capture.files.uploaded")} result={docs.data}>
86
+ {(data) => (
87
+ <div className="space-y-3">
88
+ <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />
89
+ <div className="rounded-md border border-border bg-background/55 p-3 text-sm text-muted-foreground">
90
+ {t(language, "capture.files.completed")}
91
+ </div>
92
+ </div>
93
+ )}
71
94
  </DataPanel>
72
95
  </div>
73
96
  );
74
97
  }
75
98
 
99
+ type UploadQueueItem = {
100
+ id: string;
101
+ file: File;
102
+ name: string;
103
+ size: number;
104
+ status: "queued" | "uploading" | "done" | "failed";
105
+ result?: Awaited<ReturnType<typeof latticeApi.uploadDocument>>;
106
+ };
107
+
108
+ async function uploadFiles(files: File[], setQueue: React.Dispatch<React.SetStateAction<UploadQueueItem[]>>) {
109
+ const rows = files.map((file) => ({
110
+ id: `${file.name}-${file.size}-${file.lastModified}-${Date.now()}`,
111
+ file,
112
+ name: file.name,
113
+ size: file.size,
114
+ status: "queued" as const,
115
+ }));
116
+ setQueue((items) => [...rows, ...items].slice(0, 12));
117
+ const results = [];
118
+ for (const row of rows) {
119
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: "uploading" } : item));
120
+ const result = await latticeApi.uploadDocument(row.file);
121
+ results.push(result);
122
+ setQueue((items) => items.map((item) => item.id === row.id ? { ...item, status: result.ok ? "done" : "failed", result } : item));
123
+ }
124
+ return results;
125
+ }
126
+
127
+ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onRetry: (file: File) => void }) {
128
+ const language = useAppStore((state) => state.language);
129
+ if (!queue.length) return null;
130
+ return (
131
+ <div className="mt-4 space-y-2">
132
+ {queue.map((item) => {
133
+ const detail = uploadResultDetail(item, language);
134
+ return (
135
+ <div key={item.id} className="rounded-md border border-border bg-background/55 p-3 text-sm">
136
+ <div className="flex flex-wrap items-start justify-between gap-3">
137
+ <div>
138
+ <div className="flex items-center gap-2 font-medium">
139
+ {item.status === "done" ? <CheckCircle2 className="h-4 w-4 text-emerald-400" /> : item.status === "failed" ? <AlertCircle className="h-4 w-4 text-amber-400" /> : <Loader2 className="h-4 w-4 animate-spin text-primary" />}
140
+ {item.name}
141
+ </div>
142
+ <div className="mt-1 text-xs text-muted-foreground">
143
+ {Math.max(1, Math.round(item.size / 1024))} KB · {detail}
144
+ </div>
145
+ </div>
146
+ {item.status === "failed" ? (
147
+ <Button size="sm" variant="outline" onClick={() => onRetry(item.file)}>
148
+ <RotateCcw className="h-3.5 w-3.5" /> {t(language, "capture.retry")}
149
+ </Button>
150
+ ) : null}
151
+ </div>
152
+ {item.result ? <OperationResult result={item.result} successLabel={t(language, "capture.files.success")} /> : null}
153
+ </div>
154
+ );
155
+ })}
156
+ </div>
157
+ );
158
+ }
159
+
160
+ function uploadResultDetail(item: UploadQueueItem, language: Language) {
161
+ if (item.status === "queued") return t(language, "capture.files.queued");
162
+ if (item.status === "uploading") return t(language, "capture.files.uploading");
163
+ if (!item.result?.ok) return item.result?.error || t(language, "capture.files.failed");
164
+ const data = item.result.data || {};
165
+ const node = String(data.node_id || data.graph_node || data.provenance_id || "");
166
+ return node ? t(language, "capture.files.capturedWithNode", { node }) : t(language, "capture.files.captured");
167
+ }
168
+
76
169
  function LocalPanel() {
170
+ const language = useAppStore((state) => state.language);
77
171
  const qc = useQueryClient();
78
172
  const [path, setPath] = React.useState("");
79
173
  const local = useQuery({ queryKey: ["localSources"], queryFn: latticeApi.localSources });
@@ -86,16 +180,16 @@ function LocalPanel() {
86
180
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
87
181
  <Card>
88
182
  <CardHeader>
89
- <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> Connect a folder</CardTitle>
90
- <CardDescription>Point Lattice at a folder you want it to remember.</CardDescription>
183
+ <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> {t(language, "capture.local.title")}</CardTitle>
184
+ <CardDescription>{t(language, "capture.local.description")}</CardDescription>
91
185
  </CardHeader>
92
186
  <CardContent className="space-y-3">
93
- <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="Folder path on this Mac" />
94
- <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect Folder</Button>
95
- {connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
187
+ <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder={t(language, "capture.local.placeholder")} />
188
+ <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>{t(language, "capture.local.connect")}</Button>
189
+ {connect.data ? <OperationResult result={connect.data} successLabel={t(language, "capture.local.success")} /> : null}
96
190
  </CardContent>
97
191
  </Card>
98
- <DataPanel title="Connected sources" result={local.data}>
192
+ <DataPanel title={t(language, "capture.local.sources")} result={local.data}>
99
193
  {(data) => (
100
194
  <div className="space-y-3">
101
195
  <EntityList items={(data as Record<string, unknown>).sources} titleKey="path" metaKey="status" />
@@ -110,7 +204,7 @@ function LocalPanel() {
110
204
  </div>
111
205
  )}
112
206
  </DataPanel>
113
- <DataPanel title="Folder access" result={agent.data} className="xl:col-span-2">
207
+ <DataPanel title={t(language, "capture.local.access")} result={agent.data} className="xl:col-span-2">
114
208
  {(data) => <StructuredView value={data} />}
115
209
  </DataPanel>
116
210
  </div>
@@ -118,43 +212,45 @@ function LocalPanel() {
118
212
  }
119
213
 
120
214
  function BrowserPanel() {
215
+ const language = useAppStore((state) => state.language);
121
216
  const [url, setUrl] = React.useState("");
122
217
  const read = useMutation({ mutationFn: () => latticeApi.browserReadUrl(url) });
123
218
  return (
124
219
  <Card>
125
220
  <CardHeader>
126
- <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> Save a web page</CardTitle>
127
- <CardDescription>Capture a page so Lattice can remember the useful parts.</CardDescription>
221
+ <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> {t(language, "capture.browser.title")}</CardTitle>
222
+ <CardDescription>{t(language, "capture.browser.description")}</CardDescription>
128
223
  </CardHeader>
129
224
  <CardContent className="space-y-3">
130
225
  <div className="flex flex-col gap-2 sm:flex-row">
131
- <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/article" />
132
- <Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>Capture URL</Button>
226
+ <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder={t(language, "capture.browser.placeholder")} />
227
+ <Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>{t(language, "capture.browser.capture")}</Button>
133
228
  </div>
134
- {read.data ? <OperationResult result={read.data} successLabel="URL capture requested" /> : null}
229
+ {read.data ? <OperationResult result={read.data} successLabel={t(language, "capture.browser.success")} /> : null}
135
230
  </CardContent>
136
231
  </Card>
137
232
  );
138
233
  }
139
234
 
140
235
  function PipelinePanel() {
236
+ const language = useAppStore((state) => state.language);
141
237
  const index = useQuery({ queryKey: ["index"], queryFn: latticeApi.indexStatus });
142
238
  const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
143
239
  return (
144
240
  <div className="grid gap-4 xl:grid-cols-2">
145
- <DataPanel title="Processing status" result={index.data}>
241
+ <DataPanel title={t(language, "capture.pipeline.status")} result={index.data}>
146
242
  {(data) => <StructuredView value={data} />}
147
243
  </DataPanel>
148
- <DataPanel title="Brain growth" result={stats.data}>
244
+ <DataPanel title={t(language, "capture.pipeline.growth")} result={stats.data}>
149
245
  {(data) => <StructuredView value={data} />}
150
246
  </DataPanel>
151
247
  <Card className="xl:col-span-2">
152
248
  <CardHeader>
153
- <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> Refresh memory</CardTitle>
154
- <CardDescription>Refresh search when you want Lattice to re-check captured material.</CardDescription>
249
+ <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> {t(language, "capture.pipeline.refresh")}</CardTitle>
250
+ <CardDescription>{t(language, "capture.pipeline.description")}</CardDescription>
155
251
  </CardHeader>
156
252
  <CardContent>
157
- <ActionButton label="Rebuild retrieval index" action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
253
+ <ActionButton label={t(language, "capture.pipeline.rebuild")} action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
158
254
  </CardContent>
159
255
  </Card>
160
256
  </div>
@@ -109,12 +109,20 @@ function ModelsPanel() {
109
109
  const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
110
110
  return (
111
111
  <div className="space-y-4">
112
- <StatGrid stats={[
113
- { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
114
- { label: "Memory", value: recommendation?.ram_gb ? `${String(recommendation.ram_gb)} GB` : "checking" },
115
- { label: "Top pick", value: topPick?.name || topPick?.id || "choose below" },
116
- { label: "Current", value: current?.name || currentId || "none" },
117
- ]} />
112
+ <ModelRuntimeSummary
113
+ profile={profile || {}}
114
+ recommendation={recommendation || {}}
115
+ current={current}
116
+ currentId={currentId}
117
+ latestProgress={latestProgress}
118
+ lastResult={lastResult}
119
+ onReload={() => {
120
+ if (current) void prepareModel(String(current.recommended_load_id || current.id || currentId), String(current.recommended_engine || current.engine || "local_mlx"), consent);
121
+ }}
122
+ onUnload={() => {
123
+ if (currentId) void latticeApi.unloadModel(currentId).then(() => qc.invalidateQueries({ queryKey: ["models"] }));
124
+ }}
125
+ />
118
126
  <div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
119
127
  {[
120
128
  ["Environment Analysis", true, Cpu],
@@ -296,6 +304,64 @@ function AlternativeModels({ compatibility }: { compatibility: Record<string, un
296
304
  );
297
305
  }
298
306
 
307
+ function ModelRuntimeSummary({
308
+ profile,
309
+ recommendation,
310
+ current,
311
+ currentId,
312
+ latestProgress,
313
+ lastResult,
314
+ onReload,
315
+ onUnload,
316
+ }: {
317
+ profile: Record<string, unknown>;
318
+ recommendation: Record<string, unknown>;
319
+ current?: Record<string, unknown>;
320
+ currentId: string;
321
+ latestProgress: Record<string, unknown> | null;
322
+ lastResult: Record<string, unknown> | null;
323
+ onReload: () => void;
324
+ onUnload: () => void;
325
+ }) {
326
+ const loadedName = String(current?.name || current?.id || currentId || "");
327
+ const engine = String(current?.engine || current?.recommended_engine || latestProgress?.engine || "local_mlx");
328
+ const cachePath = String(current?.local_path || current?.storage_location || recommendation.cache_path || recommendation.storage_location || "~/.cache/huggingface / ~/.latticeai/models");
329
+ const progressStage = String(latestProgress?.stage || lastResult?.stage || (loadedName ? "ready" : "idle"));
330
+ return (
331
+ <div className="space-y-3">
332
+ <StatGrid stats={[
333
+ { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
334
+ { label: "Engine", value: engine },
335
+ { label: "Loaded model", value: loadedName || "No local model loaded" },
336
+ { label: "Runtime state", value: progressStage },
337
+ ]} />
338
+ <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
339
+ <div className="flex flex-wrap items-start justify-between gap-3">
340
+ <div>
341
+ <div className="font-medium">{loadedName ? "Local model runtime is available" : "No model is loaded yet"}</div>
342
+ <div className="mt-1 text-muted-foreground">Cache/storage: {cachePath}</div>
343
+ </div>
344
+ <div className="flex flex-wrap gap-2">
345
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onReload}>Reload</Button>
346
+ <Button variant="outline" size="sm" disabled={!loadedName} onClick={onUnload}>Unload</Button>
347
+ </div>
348
+ </div>
349
+ {latestProgress ? (
350
+ <div className="mt-3">
351
+ <div className="flex justify-between text-xs text-muted-foreground">
352
+ <span>{String(latestProgress.message || "Preparing model")}</span>
353
+ <span>{Number(latestProgress.percent || 0)}%</span>
354
+ </div>
355
+ <div className="mt-1 h-2 overflow-hidden rounded-full bg-muted">
356
+ <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
357
+ </div>
358
+ </div>
359
+ ) : null}
360
+ </div>
361
+ </div>
362
+ );
363
+ }
364
+
299
365
  function ModelRecovery({ error }: { error: Record<string, unknown> }) {
300
366
  const guidance = asArray<string>(error.recovery_guidance);
301
367
  return (
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "6.2.0"
29
+ __version__ = "6.3.1"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -19,7 +19,7 @@ from datetime import datetime
19
19
  from typing import Any, Callable, Dict, List, Optional
20
20
 
21
21
 
22
- MULTI_AGENT_VERSION = "6.2.0"
22
+ MULTI_AGENT_VERSION = "6.3.1"
23
23
 
24
24
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
25
25
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "6.2.0"
3
+ __version__ = "6.3.1"