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
package/frontend/src/App.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import { Command, Menu, Moon, Search, Sun, X } from "lucide-react";
|
|
3
|
+
import { BrainCircuit, Command, Menu, Moon, Search, Sun, X } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
6
|
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
|
|
7
8
|
import { useAppStore } from "@/store/appStore";
|
|
8
9
|
import { commandRoutes, go, parseHash, primaryRoutes, PrimaryRoute } from "@/routes";
|
|
9
10
|
import { BrainPage } from "@/pages/Brain";
|
|
10
|
-
import { AskPage } from "@/pages/Ask";
|
|
11
11
|
import { CapturePage } from "@/pages/Capture";
|
|
12
12
|
import { ActPage } from "@/pages/Act";
|
|
13
13
|
import { LibraryPage } from "@/pages/Library";
|
|
@@ -25,7 +25,7 @@ function useRoute() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function Page({ primary, tab }: { primary: PrimaryRoute; tab?: string }) {
|
|
28
|
-
if (primary === "
|
|
28
|
+
if (primary === "memory") return <BrainPage initialTab="memory" />;
|
|
29
29
|
if (primary === "capture") return <CapturePage initialTab={tab} />;
|
|
30
30
|
if (primary === "act") return <ActPage initialTab={tab} />;
|
|
31
31
|
if (primary === "library") return <LibraryPage initialTab={tab} />;
|
|
@@ -33,9 +33,26 @@ function Page({ primary, tab }: { primary: PrimaryRoute; tab?: string }) {
|
|
|
33
33
|
return <BrainPage initialTab={tab} />;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function AmbientBrain() {
|
|
37
|
+
return (
|
|
38
|
+
<div className="ambient-brain" aria-hidden="true">
|
|
39
|
+
<span className="signal-line signal-line-a" />
|
|
40
|
+
<span className="signal-line signal-line-b" />
|
|
41
|
+
<span className="signal-line signal-line-c" />
|
|
42
|
+
<span className="signal-tile signal-tile-a" />
|
|
43
|
+
<span className="signal-tile signal-tile-b" />
|
|
44
|
+
<span className="signal-tile signal-tile-c" />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
37
50
|
const [query, setQuery] = React.useState("");
|
|
38
|
-
const
|
|
51
|
+
const normalized = query.trim().toLowerCase();
|
|
52
|
+
const matches = commandRoutes.filter((route) => (
|
|
53
|
+
route.label.toLowerCase().includes(normalized) || route.key.includes(normalized)
|
|
54
|
+
));
|
|
55
|
+
|
|
39
56
|
React.useEffect(() => {
|
|
40
57
|
if (!open) return;
|
|
41
58
|
const onKey = (event: KeyboardEvent) => {
|
|
@@ -44,16 +61,17 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
44
61
|
window.addEventListener("keydown", onKey);
|
|
45
62
|
return () => window.removeEventListener("keydown", onKey);
|
|
46
63
|
}, [open, onClose]);
|
|
64
|
+
|
|
47
65
|
if (!open) return null;
|
|
48
66
|
return (
|
|
49
|
-
<div className="
|
|
50
|
-
<div className="
|
|
51
|
-
<div className="
|
|
67
|
+
<div className="command-scrim" role="dialog" aria-modal="true" aria-label="Lattice command palette">
|
|
68
|
+
<div className="command-panel">
|
|
69
|
+
<div className="command-search">
|
|
52
70
|
<Search className="h-4 w-4 text-muted-foreground" />
|
|
53
|
-
<Input value={query} onChange={(
|
|
54
|
-
<Button variant="ghost" size="icon" onClick={onClose}><X className="h-4 w-4" /></Button>
|
|
71
|
+
<Input value={query} onChange={(event) => setQuery(event.target.value)} autoFocus placeholder="Jump to anything in Lattice" />
|
|
72
|
+
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close command palette"><X className="h-4 w-4" /></Button>
|
|
55
73
|
</div>
|
|
56
|
-
<div className="
|
|
74
|
+
<div className="command-list soft-scrollbar">
|
|
57
75
|
{matches.map((route) => {
|
|
58
76
|
const Icon = route.icon;
|
|
59
77
|
return (
|
|
@@ -63,10 +81,13 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
63
81
|
go(route.key);
|
|
64
82
|
onClose();
|
|
65
83
|
}}
|
|
66
|
-
className="
|
|
84
|
+
className="command-row"
|
|
67
85
|
>
|
|
68
|
-
<Icon className="h-4 w-4
|
|
69
|
-
|
|
86
|
+
<span className="command-icon"><Icon className="h-4 w-4" /></span>
|
|
87
|
+
<span>
|
|
88
|
+
<span className="block text-sm font-semibold">{route.label}</span>
|
|
89
|
+
<span className="block text-xs text-muted-foreground">Open {route.key.replace(/[-/]/g, " ")}</span>
|
|
90
|
+
</span>
|
|
70
91
|
</button>
|
|
71
92
|
);
|
|
72
93
|
})}
|
|
@@ -76,23 +97,49 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
76
97
|
);
|
|
77
98
|
}
|
|
78
99
|
|
|
100
|
+
function PrimaryDock({ active, onNavigate }: { active: PrimaryRoute; onNavigate?: () => void }) {
|
|
101
|
+
return (
|
|
102
|
+
<nav className="primary-dock" aria-label="Primary navigation">
|
|
103
|
+
{primaryRoutes.map((item) => {
|
|
104
|
+
const Icon = item.icon;
|
|
105
|
+
const selected = active === item.id;
|
|
106
|
+
return (
|
|
107
|
+
<button
|
|
108
|
+
key={item.id}
|
|
109
|
+
className={cn("dock-button", selected && "is-active")}
|
|
110
|
+
onClick={() => {
|
|
111
|
+
go(item.id);
|
|
112
|
+
onNavigate?.();
|
|
113
|
+
}}
|
|
114
|
+
aria-current={selected ? "page" : undefined}
|
|
115
|
+
>
|
|
116
|
+
<Icon className="h-4 w-4" />
|
|
117
|
+
<span>{item.label}</span>
|
|
118
|
+
</button>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</nav>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
79
125
|
export default function App() {
|
|
80
126
|
const route = useRoute();
|
|
81
|
-
const { theme, setTheme
|
|
127
|
+
const { theme, setTheme } = useAppStore();
|
|
82
128
|
const [drawer, setDrawer] = React.useState(false);
|
|
83
129
|
const [palette, setPalette] = React.useState(false);
|
|
84
|
-
const
|
|
130
|
+
const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
|
|
131
|
+
const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health, enabled: flowComplete });
|
|
85
132
|
const desktop = useQuery({
|
|
86
133
|
queryKey: ["desktopBackendStatus"],
|
|
87
134
|
queryFn: latticeApi.desktopBackendStatus,
|
|
88
|
-
enabled: Boolean(window.__TAURI_INTERNALS__),
|
|
135
|
+
enabled: flowComplete && Boolean(window.__TAURI_INTERNALS__),
|
|
89
136
|
refetchInterval: 5000,
|
|
90
137
|
});
|
|
91
|
-
const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
|
|
92
138
|
|
|
93
139
|
React.useEffect(() => {
|
|
94
140
|
document.documentElement.dataset.theme = theme;
|
|
95
141
|
}, [theme]);
|
|
142
|
+
|
|
96
143
|
React.useEffect(() => {
|
|
97
144
|
const onKey = (event: KeyboardEvent) => {
|
|
98
145
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
@@ -104,95 +151,70 @@ export default function App() {
|
|
|
104
151
|
return () => window.removeEventListener("keydown", onKey);
|
|
105
152
|
}, []);
|
|
106
153
|
|
|
154
|
+
if (!flowComplete) {
|
|
155
|
+
return <ProductFlow onComplete={() => {
|
|
156
|
+
setFlowComplete(true);
|
|
157
|
+
go("brain");
|
|
158
|
+
}} />;
|
|
159
|
+
}
|
|
160
|
+
|
|
107
161
|
const healthData = (health.data?.data || {}) as Record<string, unknown>;
|
|
108
|
-
const appVersion = typeof healthData.version === "string" ? healthData.version : null;
|
|
109
162
|
const desktopData = (desktop.data?.data || {}) as Record<string, unknown>;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const rail = (
|
|
113
|
-
<aside className="flex h-full w-64 shrink-0 flex-col border-r border-border bg-card">
|
|
114
|
-
<div className="flex h-16 items-center gap-3 border-b border-border px-4">
|
|
115
|
-
<div className="grid h-9 w-9 place-items-center rounded-md bg-primary text-primary-foreground font-bold">LA</div>
|
|
116
|
-
<div>
|
|
117
|
-
<div className="font-semibold">Lattice AI</div>
|
|
118
|
-
<div className="text-xs text-muted-foreground">Digital Brain Desktop</div>
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
<nav className="flex-1 space-y-1 overflow-auto p-3">
|
|
122
|
-
{primaryRoutes.map((item) => {
|
|
123
|
-
const Icon = item.icon;
|
|
124
|
-
const active = route.primary === item.id;
|
|
125
|
-
return (
|
|
126
|
-
<button
|
|
127
|
-
key={item.id}
|
|
128
|
-
onClick={() => {
|
|
129
|
-
go(item.id);
|
|
130
|
-
setDrawer(false);
|
|
131
|
-
}}
|
|
132
|
-
className={cn(
|
|
133
|
-
"flex min-h-14 w-full items-center gap-3 rounded-md px-3 py-2 text-left transition",
|
|
134
|
-
active ? "bg-primary/14 text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
135
|
-
)}
|
|
136
|
-
>
|
|
137
|
-
<Icon className="h-5 w-5" />
|
|
138
|
-
<span>
|
|
139
|
-
<span className="block text-sm font-medium">{item.label}</span>
|
|
140
|
-
<span className="block text-xs">{item.description}</span>
|
|
141
|
-
</span>
|
|
142
|
-
</button>
|
|
143
|
-
);
|
|
144
|
-
})}
|
|
145
|
-
</nav>
|
|
146
|
-
<div className="border-t border-border p-3 text-xs text-muted-foreground">
|
|
147
|
-
<div>Server: {health.data?.ok ? "online" : "unavailable"}</div>
|
|
148
|
-
{window.__TAURI_INTERNALS__ ? (
|
|
149
|
-
<div>Sidecar: {desktopData.running ? "running" : desktopError ? `unavailable (${desktopError})` : "starting"}</div>
|
|
150
|
-
) : null}
|
|
151
|
-
<div>Workspace: {String((workspace.data?.data as Record<string, unknown>)?.active_workspace || "local")}</div>
|
|
152
|
-
</div>
|
|
153
|
-
</aside>
|
|
154
|
-
);
|
|
163
|
+
const backendReady = Boolean(health.data?.ok);
|
|
164
|
+
const desktopReady = !window.__TAURI_INTERNALS__ || Boolean(desktopData.running);
|
|
155
165
|
|
|
156
166
|
return (
|
|
157
|
-
<div className="min-h-screen
|
|
167
|
+
<div className="app-backdrop min-h-screen text-foreground">
|
|
168
|
+
<AmbientBrain />
|
|
158
169
|
<CommandPalette open={palette} onClose={() => setPalette(false)} />
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<div className="
|
|
162
|
-
<button className="
|
|
163
|
-
<
|
|
170
|
+
|
|
171
|
+
<header className="app-chrome">
|
|
172
|
+
<div className="brand-lockup">
|
|
173
|
+
<button className="mobile-menu" onClick={() => setDrawer(true)} aria-label="Open navigation"><Menu className="h-5 w-5" /></button>
|
|
174
|
+
<button className="brand-mark" onClick={() => go("brain")} aria-label="Open Lattice Brain">
|
|
175
|
+
<BrainCircuit className="h-5 w-5" />
|
|
176
|
+
</button>
|
|
177
|
+
<div className="brand-copy">
|
|
178
|
+
<div className="brand-name">Lattice</div>
|
|
179
|
+
<div className="brand-subtitle">Living Brain</div>
|
|
180
|
+
</div>
|
|
164
181
|
</div>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
182
|
+
|
|
183
|
+
<div className="desktop-dock">
|
|
184
|
+
<PrimaryDock active={route.primary} />
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="chrome-actions">
|
|
188
|
+
<button className="status-chip" onClick={() => go("settings")}>
|
|
189
|
+
<span className={cn("status-light", backendReady && desktopReady ? "is-ready" : "is-waiting")} />
|
|
190
|
+
<span>{backendReady && desktopReady ? "Ready" : "Starting"}</span>
|
|
191
|
+
</button>
|
|
192
|
+
<Button variant="outline" onClick={() => setPalette(true)}><Command className="h-4 w-4" /> Find</Button>
|
|
193
|
+
<Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme">
|
|
194
|
+
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
|
195
|
+
</Button>
|
|
196
|
+
</div>
|
|
197
|
+
</header>
|
|
198
|
+
|
|
199
|
+
{drawer ? (
|
|
200
|
+
<div className="mobile-drawer">
|
|
201
|
+
<button className="drawer-scrim" aria-label="Close navigation" onClick={() => setDrawer(false)} />
|
|
202
|
+
<div className="drawer-panel">
|
|
203
|
+
<div className="drawer-header">
|
|
204
|
+
<div>
|
|
205
|
+
<div className="font-semibold">Lattice</div>
|
|
206
|
+
<div className="text-xs text-muted-foreground">Choose a layer</div>
|
|
207
|
+
</div>
|
|
208
|
+
<Button variant="ghost" size="icon" onClick={() => setDrawer(false)} aria-label="Close navigation"><X className="h-4 w-4" /></Button>
|
|
173
209
|
</div>
|
|
210
|
+
<PrimaryDock active={route.primary} onNavigate={() => setDrawer(false)} />
|
|
174
211
|
</div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
aria-label="Workspace mode"
|
|
182
|
-
>
|
|
183
|
-
<option value="basic">Basic</option>
|
|
184
|
-
<option value="advanced">Advanced</option>
|
|
185
|
-
<option value="admin">Admin</option>
|
|
186
|
-
</select>
|
|
187
|
-
<Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme">
|
|
188
|
-
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
|
189
|
-
</Button>
|
|
190
|
-
</div>
|
|
191
|
-
</header>
|
|
192
|
-
<main className="p-4 lg:p-6">
|
|
193
|
-
<Page primary={route.primary} tab={route.tab} />
|
|
194
|
-
</main>
|
|
195
|
-
</div>
|
|
212
|
+
</div>
|
|
213
|
+
) : null}
|
|
214
|
+
|
|
215
|
+
<main className="page-shell">
|
|
216
|
+
<Page primary={route.primary} tab={route.tab} />
|
|
217
|
+
</main>
|
|
196
218
|
</div>
|
|
197
219
|
);
|
|
198
220
|
}
|
|
@@ -77,6 +77,21 @@ function workspaceHeaders(): Record<string, string> {
|
|
|
77
77
|
return workspaceId ? { "X-Workspace-Id": workspaceId } : {};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function friendlyError(error: unknown, fallback: string) {
|
|
81
|
+
if (!error) return fallback;
|
|
82
|
+
const record = typeof error === "object" && error !== null ? error as Record<string, unknown> : null;
|
|
83
|
+
const detail = record?.detail;
|
|
84
|
+
if (typeof detail === "string") return detail;
|
|
85
|
+
const detailRecord = typeof detail === "object" && detail !== null ? detail as Record<string, unknown> : null;
|
|
86
|
+
if (detailRecord) {
|
|
87
|
+
const message = detailRecord.user_message || detailRecord.reason || detailRecord.action || detailRecord.status;
|
|
88
|
+
if (message) return String(message);
|
|
89
|
+
}
|
|
90
|
+
const message = record?.message || record?.error;
|
|
91
|
+
if (message) return String(message);
|
|
92
|
+
return fallback;
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
async function apiJson<T>(
|
|
81
96
|
method: HttpMethod,
|
|
82
97
|
path: string,
|
|
@@ -108,7 +123,7 @@ async function apiJson<T>(
|
|
|
108
123
|
status: response.status,
|
|
109
124
|
data: emptyFor(opts.shape),
|
|
110
125
|
source: "unavailable",
|
|
111
|
-
error:
|
|
126
|
+
error: friendlyError(error, response.statusText),
|
|
112
127
|
};
|
|
113
128
|
} catch (err) {
|
|
114
129
|
return {
|
|
@@ -163,6 +178,69 @@ export type ChatEventHandlers = {
|
|
|
163
178
|
signal?: AbortSignal;
|
|
164
179
|
};
|
|
165
180
|
|
|
181
|
+
export type ModelPrepareHandlers = {
|
|
182
|
+
onProgress?: (data: Record<string, unknown>) => void;
|
|
183
|
+
onDone?: (data: Record<string, unknown>) => void;
|
|
184
|
+
onError?: (data: Record<string, unknown>) => void;
|
|
185
|
+
signal?: AbortSignal;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
async function streamModelPrepare(
|
|
189
|
+
body: { model: string; engine?: string; allow_download?: boolean },
|
|
190
|
+
handlers: ModelPrepareHandlers = {},
|
|
191
|
+
) {
|
|
192
|
+
const base = await apiBase();
|
|
193
|
+
const res = await fetch(`${base}/engines/prepare-model/stream`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
credentials: "same-origin",
|
|
196
|
+
signal: handlers.signal,
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
Accept: "text/event-stream",
|
|
200
|
+
...workspaceHeaders(),
|
|
201
|
+
} satisfies HeadersInit,
|
|
202
|
+
body: JSON.stringify({ engine: null, allow_download: false, ...body }),
|
|
203
|
+
});
|
|
204
|
+
if (!res.ok || !res.body || !(res.headers.get("content-type") || "").includes("text/event-stream")) {
|
|
205
|
+
const payload = await res.json().catch(() => null);
|
|
206
|
+
const detail = payload?.detail && typeof payload.detail === "object" ? payload.detail : payload;
|
|
207
|
+
const message = friendlyError(payload, res.statusText);
|
|
208
|
+
handlers.onError?.({ status: "error", user_message: message, ...(detail || {}) });
|
|
209
|
+
return { source: "live" as const, ok: false, status: res.status, data: detail || {}, error: message };
|
|
210
|
+
}
|
|
211
|
+
const reader = res.body.getReader();
|
|
212
|
+
const decoder = new TextDecoder();
|
|
213
|
+
let buffer = "";
|
|
214
|
+
let eventName = "message";
|
|
215
|
+
let finalData: Record<string, unknown> = {};
|
|
216
|
+
for (;;) {
|
|
217
|
+
const { done, value } = await reader.read();
|
|
218
|
+
if (done) break;
|
|
219
|
+
buffer += decoder.decode(value, { stream: true });
|
|
220
|
+
const parts = buffer.split("\n\n");
|
|
221
|
+
buffer = parts.pop() || "";
|
|
222
|
+
for (const part of parts) {
|
|
223
|
+
const lines = part.split("\n");
|
|
224
|
+
eventName = lines.find((item) => item.startsWith("event:"))?.slice(6).trim() || "message";
|
|
225
|
+
const dataLine = lines.find((item) => item.startsWith("data:"));
|
|
226
|
+
if (!dataLine) continue;
|
|
227
|
+
const raw = dataLine.slice(5).trim();
|
|
228
|
+
const data = raw ? JSON.parse(raw) as Record<string, unknown> : {};
|
|
229
|
+
if (eventName === "progress") handlers.onProgress?.(data);
|
|
230
|
+
if (eventName === "error") {
|
|
231
|
+
const detail = typeof data.detail === "object" && data.detail !== null ? data.detail as Record<string, unknown> : data;
|
|
232
|
+
handlers.onError?.(detail);
|
|
233
|
+
return { source: "live" as const, ok: false, status: Number(data.status_code || 500), data: detail, error: friendlyError({ detail }, "Model setup failed") };
|
|
234
|
+
}
|
|
235
|
+
if (eventName === "done") {
|
|
236
|
+
finalData = data;
|
|
237
|
+
handlers.onDone?.(data);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { source: "live" as const, ok: true, status: 200, data: finalData };
|
|
242
|
+
}
|
|
243
|
+
|
|
166
244
|
async function streamChat(body: Record<string, unknown>, handlers: ChatEventHandlers = {}) {
|
|
167
245
|
const base = await apiBase();
|
|
168
246
|
const res = await fetch(`${base}/chat`, {
|
|
@@ -263,6 +341,11 @@ export const latticeApi = {
|
|
|
263
341
|
connectFolder: (path: string) => post("/knowledge-graph/local/index", { path, approved: true, watch_enabled: true, consent: { approved: true, source: "desktop-spa" } }, {}),
|
|
264
342
|
localWatchStop: (source_id: string) => post("/knowledge-graph/local/watch/stop", { source_id }, {}),
|
|
265
343
|
models: () => get("/models", { catalog: [], loaded: [], recommended: [] }),
|
|
344
|
+
setupScan: () => get("/setup/scan", { environment: {}, recommendations: {}, zero_config: {} }),
|
|
345
|
+
modelRecommendations: (engine = "local_mlx") => get("/models/recommendations", { profile: {}, recommendations: { models: [], families: [], counts: {} } }, { engine }),
|
|
346
|
+
installEngine: (engine: string) => post("/engines/install", { engine }, {}),
|
|
347
|
+
prepareModel: (model: string, engine?: string, allow_download = false) => post("/engines/prepare-model", { model, engine: engine || null, allow_download }, {}),
|
|
348
|
+
streamModelPrepare,
|
|
266
349
|
loadModel: (model_id: string, engine?: string, allow_download = false) => post("/models/load", { model_id, engine: engine || null, allow_download }, {}),
|
|
267
350
|
unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
|
|
268
351
|
embeddingsStatus: () => get("/api/embeddings/status", {}),
|