ltcai 4.3.3 → 4.5.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.
- package/README.md +53 -20
- package/docs/CHANGELOG.md +122 -0
- package/docs/V4_4_0_EXTRACTION_REPORT.md +239 -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_DIGITAL_BRAIN_RECOVERY.md +16 -16
- package/docs/architecture.md +8 -4
- package/frontend/src/App.tsx +152 -91
- package/frontend/src/api/client.ts +83 -1
- package/frontend/src/components/FirstRunGuide.tsx +99 -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 +51 -19
- package/frontend/src/pages/Brain.tsx +60 -42
- 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 +15 -13
- package/frontend/src/store/appStore.ts +8 -1
- package/frontend/src/styles.css +666 -36
- package/lattice_brain/__init__.py +38 -23
- package/lattice_brain/_kg_common.py +11 -1
- package/lattice_brain/context.py +212 -2
- package/lattice_brain/conversations.py +234 -1
- package/lattice_brain/discovery.py +11 -1
- package/lattice_brain/documents.py +11 -1
- package/lattice_brain/graph/__init__.py +28 -0
- package/lattice_brain/graph/_kg_common.py +1123 -0
- package/lattice_brain/graph/curator.py +473 -0
- package/lattice_brain/graph/discovery.py +1455 -0
- package/lattice_brain/graph/documents.py +218 -0
- package/lattice_brain/graph/identity.py +175 -0
- package/lattice_brain/graph/ingest.py +644 -0
- package/lattice_brain/graph/network.py +205 -0
- package/lattice_brain/graph/projection.py +571 -0
- package/lattice_brain/graph/provenance.py +401 -0
- package/lattice_brain/graph/retrieval.py +1341 -0
- package/lattice_brain/graph/schema.py +640 -0
- package/lattice_brain/graph/store.py +237 -0
- package/lattice_brain/graph/write_master.py +225 -0
- package/lattice_brain/identity.py +11 -13
- package/lattice_brain/ingest.py +11 -1
- package/lattice_brain/ingestion.py +318 -0
- package/lattice_brain/memory.py +100 -1
- package/lattice_brain/network.py +11 -1
- package/lattice_brain/portability.py +431 -0
- package/lattice_brain/projection.py +11 -1
- package/lattice_brain/provenance.py +11 -1
- package/lattice_brain/retrieval.py +11 -1
- package/lattice_brain/runtime/__init__.py +32 -0
- package/lattice_brain/runtime/agent_runtime.py +569 -0
- package/lattice_brain/runtime/hooks.py +754 -0
- package/lattice_brain/runtime/multi_agent.py +795 -0
- package/lattice_brain/schema.py +11 -1
- package/lattice_brain/store.py +10 -2
- package/lattice_brain/workflow.py +461 -0
- package/lattice_brain/write_master.py +11 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +2 -2
- package/latticeai/api/browser.py +1 -1
- package/latticeai/api/chat.py +1 -1
- package/latticeai/api/computer_use.py +1 -1
- package/latticeai/api/hooks.py +2 -2
- package/latticeai/api/mcp.py +1 -1
- package/latticeai/api/models.py +107 -18
- package/latticeai/api/tools.py +1 -1
- package/latticeai/api/workflow_designer.py +2 -2
- package/latticeai/app_factory.py +4 -4
- package/latticeai/brain/__init__.py +24 -6
- package/latticeai/brain/_kg_common.py +11 -1117
- package/latticeai/brain/context.py +12 -208
- package/latticeai/brain/conversations.py +12 -231
- package/latticeai/brain/discovery.py +13 -1451
- package/latticeai/brain/documents.py +13 -214
- package/latticeai/brain/identity.py +11 -169
- package/latticeai/brain/ingest.py +13 -640
- package/latticeai/brain/memory.py +12 -97
- package/latticeai/brain/network.py +12 -200
- package/latticeai/brain/projection.py +13 -567
- package/latticeai/brain/provenance.py +13 -397
- package/latticeai/brain/retrieval.py +13 -1337
- package/latticeai/brain/schema.py +12 -635
- package/latticeai/brain/store.py +13 -233
- package/latticeai/brain/write_master.py +13 -221
- package/latticeai/core/agent.py +1 -1
- package/latticeai/core/agent_registry.py +2 -2
- package/latticeai/core/builtin_hooks.py +2 -2
- package/latticeai/core/graph_curator.py +6 -468
- package/latticeai/core/hooks.py +6 -749
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/model_compat.py +250 -0
- package/latticeai/core/multi_agent.py +6 -790
- package/latticeai/core/workflow_engine.py +6 -456
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +136 -32
- package/latticeai/services/agent_runtime.py +6 -564
- package/latticeai/services/ingestion.py +6 -313
- package/latticeai/services/kg_portability.py +6 -426
- 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/latticeai/services/platform_runtime.py +3 -3
- package/latticeai/services/run_executor.py +1 -1
- package/latticeai/services/upload_service.py +1 -1
- package/p_reinforce.py +1 -1
- package/package.json +1 -1
- package/scripts/build_frontend_assets.mjs +12 -1
- package/scripts/bump_version.py +1 -1
- package/scripts/wheel_smoke.py +7 -0
- 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-3G8qcrIS.js +336 -0
- package/static/app/assets/index-3G8qcrIS.js.map +1 -0
- package/static/app/assets/index-C0wYZp7k.css +2 -0
- package/static/app/index.html +2 -2
- 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,10 +1,11 @@
|
|
|
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, CheckCircle2, Command, Menu, Moon, Search, Sparkles, 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 {
|
|
7
|
+
import { FirstRunGuide } from "@/components/FirstRunGuide";
|
|
8
|
+
import { useAppStore, WorkspaceMode } from "@/store/appStore";
|
|
8
9
|
import { commandRoutes, go, parseHash, primaryRoutes, PrimaryRoute } from "@/routes";
|
|
9
10
|
import { BrainPage } from "@/pages/Brain";
|
|
10
11
|
import { AskPage } from "@/pages/Ask";
|
|
@@ -14,6 +15,12 @@ import { LibraryPage } from "@/pages/Library";
|
|
|
14
15
|
import { SystemPage } from "@/pages/System";
|
|
15
16
|
import { cn } from "@/lib/utils";
|
|
16
17
|
|
|
18
|
+
const modes: Array<{ id: WorkspaceMode; label: string }> = [
|
|
19
|
+
{ id: "basic", label: "Calm" },
|
|
20
|
+
{ id: "advanced", label: "Deep" },
|
|
21
|
+
{ id: "admin", label: "Admin" },
|
|
22
|
+
];
|
|
23
|
+
|
|
17
24
|
function useRoute() {
|
|
18
25
|
const [route, setRoute] = React.useState(parseHash);
|
|
19
26
|
React.useEffect(() => {
|
|
@@ -33,9 +40,26 @@ function Page({ primary, tab }: { primary: PrimaryRoute; tab?: string }) {
|
|
|
33
40
|
return <BrainPage initialTab={tab} />;
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
function AmbientBrain() {
|
|
44
|
+
return (
|
|
45
|
+
<div className="ambient-brain" aria-hidden="true">
|
|
46
|
+
<span className="signal-line signal-line-a" />
|
|
47
|
+
<span className="signal-line signal-line-b" />
|
|
48
|
+
<span className="signal-line signal-line-c" />
|
|
49
|
+
<span className="signal-tile signal-tile-a" />
|
|
50
|
+
<span className="signal-tile signal-tile-b" />
|
|
51
|
+
<span className="signal-tile signal-tile-c" />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
37
57
|
const [query, setQuery] = React.useState("");
|
|
38
|
-
const
|
|
58
|
+
const normalized = query.trim().toLowerCase();
|
|
59
|
+
const matches = commandRoutes.filter((route) => (
|
|
60
|
+
route.label.toLowerCase().includes(normalized) || route.key.includes(normalized)
|
|
61
|
+
));
|
|
62
|
+
|
|
39
63
|
React.useEffect(() => {
|
|
40
64
|
if (!open) return;
|
|
41
65
|
const onKey = (event: KeyboardEvent) => {
|
|
@@ -44,16 +68,17 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
44
68
|
window.addEventListener("keydown", onKey);
|
|
45
69
|
return () => window.removeEventListener("keydown", onKey);
|
|
46
70
|
}, [open, onClose]);
|
|
71
|
+
|
|
47
72
|
if (!open) return null;
|
|
48
73
|
return (
|
|
49
|
-
<div className="
|
|
50
|
-
<div className="
|
|
51
|
-
<div className="
|
|
74
|
+
<div className="command-scrim" role="dialog" aria-modal="true" aria-label="Lattice command palette">
|
|
75
|
+
<div className="command-panel">
|
|
76
|
+
<div className="command-search">
|
|
52
77
|
<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>
|
|
78
|
+
<Input value={query} onChange={(event) => setQuery(event.target.value)} autoFocus placeholder="Jump to anything in Lattice" />
|
|
79
|
+
<Button variant="ghost" size="icon" onClick={onClose} aria-label="Close command palette"><X className="h-4 w-4" /></Button>
|
|
55
80
|
</div>
|
|
56
|
-
<div className="
|
|
81
|
+
<div className="command-list soft-scrollbar">
|
|
57
82
|
{matches.map((route) => {
|
|
58
83
|
const Icon = route.icon;
|
|
59
84
|
return (
|
|
@@ -63,10 +88,13 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
63
88
|
go(route.key);
|
|
64
89
|
onClose();
|
|
65
90
|
}}
|
|
66
|
-
className="
|
|
91
|
+
className="command-row"
|
|
67
92
|
>
|
|
68
|
-
<Icon className="h-4 w-4
|
|
69
|
-
|
|
93
|
+
<span className="command-icon"><Icon className="h-4 w-4" /></span>
|
|
94
|
+
<span>
|
|
95
|
+
<span className="block text-sm font-semibold">{route.label}</span>
|
|
96
|
+
<span className="block text-xs text-muted-foreground">Open {route.key.replace(/[-/]/g, " ")}</span>
|
|
97
|
+
</span>
|
|
70
98
|
</button>
|
|
71
99
|
);
|
|
72
100
|
})}
|
|
@@ -76,6 +104,48 @@ function CommandPalette({ open, onClose }: { open: boolean; onClose: () => void
|
|
|
76
104
|
);
|
|
77
105
|
}
|
|
78
106
|
|
|
107
|
+
function PrimaryDock({ active, onNavigate }: { active: PrimaryRoute; onNavigate?: () => void }) {
|
|
108
|
+
return (
|
|
109
|
+
<nav className="primary-dock" aria-label="Primary navigation">
|
|
110
|
+
{primaryRoutes.map((item) => {
|
|
111
|
+
const Icon = item.icon;
|
|
112
|
+
const selected = active === item.id;
|
|
113
|
+
return (
|
|
114
|
+
<button
|
|
115
|
+
key={item.id}
|
|
116
|
+
className={cn("dock-button", selected && "is-active")}
|
|
117
|
+
onClick={() => {
|
|
118
|
+
go(item.id);
|
|
119
|
+
onNavigate?.();
|
|
120
|
+
}}
|
|
121
|
+
aria-current={selected ? "page" : undefined}
|
|
122
|
+
>
|
|
123
|
+
<Icon className="h-4 w-4" />
|
|
124
|
+
<span>{item.label}</span>
|
|
125
|
+
</button>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</nav>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function ModeSwitch({ mode, setMode }: { mode: WorkspaceMode; setMode: (mode: WorkspaceMode) => void }) {
|
|
133
|
+
return (
|
|
134
|
+
<div className="mode-switch" aria-label="Experience mode">
|
|
135
|
+
{modes.map((item) => (
|
|
136
|
+
<button
|
|
137
|
+
key={item.id}
|
|
138
|
+
className={cn(mode === item.id && "is-active")}
|
|
139
|
+
onClick={() => setMode(item.id)}
|
|
140
|
+
aria-pressed={mode === item.id}
|
|
141
|
+
>
|
|
142
|
+
{item.label}
|
|
143
|
+
</button>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
79
149
|
export default function App() {
|
|
80
150
|
const route = useRoute();
|
|
81
151
|
const { theme, setTheme, mode, setMode } = useAppStore();
|
|
@@ -93,6 +163,7 @@ export default function App() {
|
|
|
93
163
|
React.useEffect(() => {
|
|
94
164
|
document.documentElement.dataset.theme = theme;
|
|
95
165
|
}, [theme]);
|
|
166
|
+
|
|
96
167
|
React.useEffect(() => {
|
|
97
168
|
const onKey = (event: KeyboardEvent) => {
|
|
98
169
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
@@ -105,94 +176,84 @@ export default function App() {
|
|
|
105
176
|
}, []);
|
|
106
177
|
|
|
107
178
|
const healthData = (health.data?.data || {}) as Record<string, unknown>;
|
|
108
|
-
const
|
|
179
|
+
const workspaceData = (workspace.data?.data || {}) as Record<string, unknown>;
|
|
109
180
|
const desktopData = (desktop.data?.data || {}) as Record<string, unknown>;
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
);
|
|
181
|
+
const appVersion = typeof healthData.version === "string" ? healthData.version : null;
|
|
182
|
+
const activeRoute = primaryRoutes.find((item) => item.id === route.primary);
|
|
183
|
+
const workspaceName = String(workspaceData.active_workspace || "Personal space");
|
|
184
|
+
const backendReady = Boolean(health.data?.ok);
|
|
185
|
+
const desktopReady = !window.__TAURI_INTERNALS__ || Boolean(desktopData.running);
|
|
155
186
|
|
|
156
187
|
return (
|
|
157
|
-
<div className="min-h-screen
|
|
188
|
+
<div className="app-backdrop min-h-screen text-foreground">
|
|
189
|
+
<AmbientBrain />
|
|
158
190
|
<CommandPalette open={palette} onClose={() => setPalette(false)} />
|
|
159
|
-
|
|
191
|
+
|
|
192
|
+
<header className="app-chrome">
|
|
193
|
+
<div className="brand-lockup">
|
|
194
|
+
<button className="mobile-menu" onClick={() => setDrawer(true)} aria-label="Open navigation"><Menu className="h-5 w-5" /></button>
|
|
195
|
+
<button className="brand-mark" onClick={() => go("brain")} aria-label="Open Lattice home">
|
|
196
|
+
<BrainCircuit className="h-5 w-5" />
|
|
197
|
+
</button>
|
|
198
|
+
<div className="brand-copy">
|
|
199
|
+
<div className="brand-name">Lattice</div>
|
|
200
|
+
<div className="brand-subtitle">Digital Brain</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div className="desktop-dock">
|
|
205
|
+
<PrimaryDock active={route.primary} />
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="chrome-actions">
|
|
209
|
+
<button className="status-chip" onClick={() => go("settings")}>
|
|
210
|
+
<span className={cn("status-light", backendReady && desktopReady ? "is-ready" : "is-waiting")} />
|
|
211
|
+
<span>{backendReady && desktopReady ? "Ready" : "Starting"}</span>
|
|
212
|
+
</button>
|
|
213
|
+
<Button variant="outline" onClick={() => setPalette(true)}><Command className="h-4 w-4" /> Find</Button>
|
|
214
|
+
<Button variant="outline" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} aria-label="Toggle theme">
|
|
215
|
+
{theme === "dark" ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
218
|
+
</header>
|
|
219
|
+
|
|
160
220
|
{drawer ? (
|
|
161
|
-
<div className="
|
|
162
|
-
<button className="
|
|
163
|
-
<div className="
|
|
221
|
+
<div className="mobile-drawer">
|
|
222
|
+
<button className="drawer-scrim" aria-label="Close navigation" onClick={() => setDrawer(false)} />
|
|
223
|
+
<div className="drawer-panel">
|
|
224
|
+
<div className="drawer-header">
|
|
225
|
+
<div>
|
|
226
|
+
<div className="font-semibold">Lattice</div>
|
|
227
|
+
<div className="text-xs text-muted-foreground">Choose a room</div>
|
|
228
|
+
</div>
|
|
229
|
+
<Button variant="ghost" size="icon" onClick={() => setDrawer(false)} aria-label="Close navigation"><X className="h-4 w-4" /></Button>
|
|
230
|
+
</div>
|
|
231
|
+
<PrimaryDock active={route.primary} onNavigate={() => setDrawer(false)} />
|
|
232
|
+
</div>
|
|
164
233
|
</div>
|
|
165
234
|
) : null}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<div className="
|
|
171
|
-
|
|
172
|
-
<div className="truncate font-medium">{primaryRoutes.find((item) => item.id === route.primary)?.label}</div>
|
|
173
|
-
</div>
|
|
235
|
+
|
|
236
|
+
<main className="page-shell">
|
|
237
|
+
<section className="workspace-ribbon" aria-label="Current workspace">
|
|
238
|
+
<div className="min-w-0">
|
|
239
|
+
<div className="ribbon-kicker"><Sparkles className="h-4 w-4" /> {activeRoute?.label || "Home"}</div>
|
|
240
|
+
<h1>{activeRoute?.description || "A calm place to think with your knowledge."}</h1>
|
|
174
241
|
</div>
|
|
175
|
-
<div className="
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
>
|
|
183
|
-
|
|
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>
|
|
242
|
+
<div className="ribbon-meta">
|
|
243
|
+
<div className="meta-card">
|
|
244
|
+
<CheckCircle2 className="h-4 w-4 text-primary" />
|
|
245
|
+
<span>{workspaceName}</span>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="meta-card">
|
|
248
|
+
<span>{appVersion ? `v${appVersion}` : "Version checking"}</span>
|
|
249
|
+
</div>
|
|
250
|
+
<ModeSwitch mode={mode} setMode={setMode} />
|
|
190
251
|
</div>
|
|
191
|
-
</
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
</
|
|
252
|
+
</section>
|
|
253
|
+
|
|
254
|
+
<FirstRunGuide />
|
|
255
|
+
<Page primary={route.primary} tab={route.tab} />
|
|
256
|
+
</main>
|
|
196
257
|
</div>
|
|
197
258
|
);
|
|
198
259
|
}
|
|
@@ -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,10 @@ 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
|
+
modelRecommendations: (engine = "local_mlx") => get("/models/recommendations", { profile: {}, recommendations: { models: [], families: [], counts: {} } }, { engine }),
|
|
345
|
+
installEngine: (engine: string) => post("/engines/install", { engine }, {}),
|
|
346
|
+
prepareModel: (model: string, engine?: string, allow_download = false) => post("/engines/prepare-model", { model, engine: engine || null, allow_download }, {}),
|
|
347
|
+
streamModelPrepare,
|
|
266
348
|
loadModel: (model_id: string, engine?: string, allow_download = false) => post("/models/load", { model_id, engine: engine || null, allow_download }, {}),
|
|
267
349
|
unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
|
|
268
350
|
embeddingsStatus: () => get("/api/embeddings/status", {}),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ArrowRight, CheckCircle2, Cpu, Download, Layers3, Library, PlayCircle, SlidersHorizontal, UserCircle, Users } from "lucide-react";
|
|
4
|
+
import { latticeApi } from "@/api/client";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { useAppStore } from "@/store/appStore";
|
|
8
|
+
import { go } from "@/routes";
|
|
9
|
+
import { asArray } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
function readDismissed() {
|
|
12
|
+
try {
|
|
13
|
+
return localStorage.getItem("lattice.onboarding.dismissed") === "true";
|
|
14
|
+
} catch {}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function FirstRunGuide() {
|
|
19
|
+
const [dismissed, setDismissed] = React.useState(readDismissed);
|
|
20
|
+
const mode = useAppStore((state) => state.mode);
|
|
21
|
+
const profile = useQuery({ queryKey: ["profile"], queryFn: latticeApi.profile });
|
|
22
|
+
const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
|
|
23
|
+
const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
24
|
+
const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
|
|
25
|
+
if (dismissed) return null;
|
|
26
|
+
|
|
27
|
+
const profileData = (profile.data?.data || {}) as Record<string, unknown>;
|
|
28
|
+
const workspaceData = (workspace.data?.data || {}) as Record<string, unknown>;
|
|
29
|
+
const registry = (workspaceData.workspace_registry || {}) as Record<string, unknown>;
|
|
30
|
+
const modelData = (models.data?.data || {}) as Record<string, unknown>;
|
|
31
|
+
const recommendationData = ((recs.data?.data as Record<string, unknown> | undefined)?.recommendations || {}) as Record<string, unknown>;
|
|
32
|
+
const currentModel = String(modelData.current || "");
|
|
33
|
+
const loadedModels = asArray(modelData.loaded);
|
|
34
|
+
const topPick = recommendationData.top_pick as Record<string, unknown> | undefined;
|
|
35
|
+
const compatProfiles = asArray<Record<string, unknown>>(modelData.compat_profiles);
|
|
36
|
+
const readyProfile = compatProfiles.some((item) => item.chat_compatible || item.quality_status === "ok" || item.quality_status === "degraded");
|
|
37
|
+
|
|
38
|
+
const steps = [
|
|
39
|
+
{ label: "Make it yours", done: Boolean(profileData.email), icon: UserCircle, action: "account", detail: "Sign in or keep a local profile." },
|
|
40
|
+
{ label: "Choose a space", done: Boolean(registry.active_workspace || workspaceData.active_workspace), icon: Users, action: "workspace-admin", detail: "Decide where memories belong." },
|
|
41
|
+
{ label: "Meet your Mac", done: recs.isSuccess, icon: Cpu, action: "models", detail: "Let Lattice inspect what can run locally." },
|
|
42
|
+
{ label: "Pick a brain", done: Boolean(topPick || currentModel), icon: Library, action: "models", detail: "Use the recommended local model." },
|
|
43
|
+
{ label: "Install locally", done: Boolean(currentModel || loadedModels.length), icon: Download, action: "models", detail: "Download only with explicit consent." },
|
|
44
|
+
{ label: "Try a question", done: Boolean(readyProfile || currentModel || loadedModels.length), icon: PlayCircle, action: "chat", detail: "Confirm the model can answer." },
|
|
45
|
+
{ label: "Set the pace", done: Boolean(mode), icon: SlidersHorizontal, action: "settings", detail: "Stay Calm or switch deeper." },
|
|
46
|
+
{ label: "Explore memory", done: true, icon: Layers3, action: "knowledge-graph", detail: "Open the living map." },
|
|
47
|
+
];
|
|
48
|
+
const completed = steps.filter((step) => step.done).length;
|
|
49
|
+
const nextStep = steps.find((step) => !step.done) || steps[steps.length - 1];
|
|
50
|
+
const progress = Math.round((completed / steps.length) * 100);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<section className="arrival-panel" aria-label="First 10 minutes">
|
|
54
|
+
<div className="arrival-copy">
|
|
55
|
+
<div className="page-kicker"><CheckCircle2 className="h-4 w-4" /> First 10 minutes</div>
|
|
56
|
+
<h2>Build your Digital Brain without guessing.</h2>
|
|
57
|
+
<p>
|
|
58
|
+
Start with a space, let Lattice recommend a private local model, then add the first pieces of knowledge.
|
|
59
|
+
Every step keeps the next action visible.
|
|
60
|
+
</p>
|
|
61
|
+
<div className="arrival-actions">
|
|
62
|
+
<Button onClick={() => go(nextStep.action)}>{nextStep.done ? "Open memory map" : `Continue: ${nextStep.label}`}</Button>
|
|
63
|
+
<Button variant="outline" onClick={() => go("models")}>Set up model</Button>
|
|
64
|
+
<Button variant="ghost" onClick={() => {
|
|
65
|
+
try { localStorage.setItem("lattice.onboarding.dismissed", "true"); } catch {}
|
|
66
|
+
setDismissed(true);
|
|
67
|
+
}}>
|
|
68
|
+
Hide
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="journey-panel">
|
|
73
|
+
<div className="journey-head">
|
|
74
|
+
<div>
|
|
75
|
+
<div className="text-sm font-semibold">{completed} of {steps.length} ready</div>
|
|
76
|
+
<div className="text-xs text-muted-foreground">{mode === "basic" ? "Calm mode" : `${mode} mode`}</div>
|
|
77
|
+
</div>
|
|
78
|
+
<Badge variant={progress === 100 ? "success" : "warning"}>{progress}%</Badge>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="journey-progress"><span style={{ width: `${progress}%` }} /></div>
|
|
81
|
+
<div className="journey-steps">
|
|
82
|
+
{steps.map((step) => {
|
|
83
|
+
const Icon = step.icon;
|
|
84
|
+
return (
|
|
85
|
+
<button key={step.label} onClick={() => go(step.action)} className="journey-step">
|
|
86
|
+
<span className="journey-icon"><Icon className="h-4 w-4" /></span>
|
|
87
|
+
<span className="min-w-0">
|
|
88
|
+
<span className="block truncate text-sm font-semibold">{step.label}</span>
|
|
89
|
+
<span className="block truncate text-xs text-muted-foreground">{step.detail}</span>
|
|
90
|
+
</span>
|
|
91
|
+
<ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
|
|
92
|
+
</button>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
);
|
|
99
|
+
}
|