ltcai 4.4.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +77 -33
  2. package/docs/CHANGELOG.md +128 -0
  3. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
  4. package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
  5. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
  6. package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
  7. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
  8. package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
  9. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
  10. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
  11. package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
  12. package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
  13. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
  14. package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
  15. package/docs/V4_5_1_UX_REPORT.md +45 -0
  16. package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
  17. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
  18. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +58 -0
  19. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +18 -17
  20. package/docs/architecture.md +8 -4
  21. package/frontend/index.html +2 -2
  22. package/frontend/src/App.tsx +120 -98
  23. package/frontend/src/api/client.ts +84 -1
  24. package/frontend/src/components/BrainConversation.tsx +301 -0
  25. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  26. package/frontend/src/components/LivingBrain.tsx +121 -0
  27. package/frontend/src/components/ProductFlow.tsx +596 -0
  28. package/frontend/src/components/primitives.tsx +131 -25
  29. package/frontend/src/components/ui/badge.tsx +2 -2
  30. package/frontend/src/components/ui/button.tsx +7 -7
  31. package/frontend/src/components/ui/card.tsx +5 -5
  32. package/frontend/src/components/ui/input.tsx +1 -1
  33. package/frontend/src/components/ui/textarea.tsx +1 -1
  34. package/frontend/src/pages/Act.tsx +58 -28
  35. package/frontend/src/pages/Ask.tsx +2 -197
  36. package/frontend/src/pages/Brain.tsx +108 -71
  37. package/frontend/src/pages/Capture.tsx +24 -24
  38. package/frontend/src/pages/Library.tsx +222 -32
  39. package/frontend/src/pages/System.tsx +56 -34
  40. package/frontend/src/routes.ts +16 -25
  41. package/frontend/src/store/appStore.ts +8 -1
  42. package/frontend/src/styles.css +1663 -36
  43. package/lattice_brain/__init__.py +1 -1
  44. package/lattice_brain/runtime/multi_agent.py +1 -1
  45. package/latticeai/__init__.py +1 -1
  46. package/latticeai/api/models.py +107 -18
  47. package/latticeai/core/marketplace.py +1 -1
  48. package/latticeai/core/model_compat.py +250 -0
  49. package/latticeai/core/workspace_os.py +1 -1
  50. package/latticeai/models/router.py +136 -32
  51. package/latticeai/services/model_catalog.py +2 -2
  52. package/latticeai/services/model_recommendation.py +8 -1
  53. package/latticeai/services/model_runtime.py +18 -3
  54. package/package.json +2 -2
  55. package/scripts/build_frontend_assets.mjs +12 -1
  56. package/src-tauri/Cargo.lock +1 -1
  57. package/src-tauri/Cargo.toml +1 -1
  58. package/src-tauri/tauri.conf.json +1 -1
  59. package/static/app/asset-manifest.json +5 -5
  60. package/static/app/assets/index-By-G-Kay.css +2 -0
  61. package/static/app/assets/index-CJx6WuQH.js +336 -0
  62. package/static/app/assets/index-CJx6WuQH.js.map +1 -0
  63. package/static/app/index.html +4 -4
  64. package/static/manifest.json +1 -1
  65. package/static/app/assets/index-CHHal8Zl.css +0 -2
  66. package/static/app/assets/index-pdzil9ac.js +0 -333
  67. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -1,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 === "ask") return <AskPage />;
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 matches = commandRoutes.filter((route) => route.label.toLowerCase().includes(query.toLowerCase()) || route.key.includes(query.toLowerCase()));
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="fixed inset-0 z-50 bg-background/80 p-4 backdrop-blur-sm" role="dialog" aria-modal="true">
50
- <div className="mx-auto mt-16 max-w-xl rounded-lg border border-border bg-card shadow-xl">
51
- <div className="flex items-center gap-2 border-b border-border p-3">
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={(e) => setQuery(e.target.value)} autoFocus placeholder="Jump to a capability" />
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="max-h-96 overflow-auto p-2">
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="flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-muted"
84
+ className="command-row"
67
85
  >
68
- <Icon className="h-4 w-4 text-primary" />
69
- {route.label}
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, mode, setMode } = useAppStore();
127
+ const { theme, setTheme } = useAppStore();
82
128
  const [drawer, setDrawer] = React.useState(false);
83
129
  const [palette, setPalette] = React.useState(false);
84
- const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health });
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 desktopError = typeof desktopData.last_error === "string" ? desktopData.last_error : desktop.data?.error;
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 bg-background text-foreground">
167
+ <div className="app-backdrop min-h-screen text-foreground">
168
+ <AmbientBrain />
158
169
  <CommandPalette open={palette} onClose={() => setPalette(false)} />
159
- <div className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:block">{rail}</div>
160
- {drawer ? (
161
- <div className="fixed inset-0 z-40 lg:hidden">
162
- <button className="absolute inset-0 bg-background/70" aria-label="Close navigation" onClick={() => setDrawer(false)} />
163
- <div className="relative h-full">{rail}</div>
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
- ) : null}
166
- <div className="lg:pl-64">
167
- <header className="sticky top-0 z-30 flex h-16 items-center justify-between gap-3 border-b border-border bg-background/95 px-4 backdrop-blur">
168
- <div className="flex min-w-0 items-center gap-2">
169
- <Button variant="ghost" size="icon" className="lg:hidden" onClick={() => setDrawer(true)}><Menu className="h-5 w-5" /></Button>
170
- <div className="min-w-0">
171
- <div className="truncate text-sm text-muted-foreground">{appVersion ? `v${appVersion}` : "Version unavailable"}</div>
172
- <div className="truncate font-medium">{primaryRoutes.find((item) => item.id === route.primary)?.label}</div>
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
- <div className="flex items-center gap-2">
176
- <Button variant="outline" onClick={() => setPalette(true)}><Command className="h-4 w-4" /> Search</Button>
177
- <select
178
- value={mode}
179
- onChange={(e) => setMode(e.target.value as "basic" | "advanced" | "admin")}
180
- className="h-9 rounded-md border border-border bg-background px-2 text-sm"
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: error ? JSON.stringify(error) : response.statusText,
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", {}),