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
|
@@ -12,9 +12,9 @@ type CaptureTab = "files" | "local" | "browser" | "pipeline";
|
|
|
12
12
|
|
|
13
13
|
const tabs: Array<{ id: CaptureTab; label: string }> = [
|
|
14
14
|
{ id: "files", label: "Files" },
|
|
15
|
-
{ id: "local", label: "
|
|
16
|
-
{ id: "browser", label: "Web
|
|
17
|
-
{ id: "pipeline", label: "
|
|
15
|
+
{ id: "local", label: "Folders" },
|
|
16
|
+
{ id: "browser", label: "Web" },
|
|
17
|
+
{ id: "pipeline", label: "Flow" },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
export function CapturePage({ initialTab }: { initialTab?: string }) {
|
|
@@ -23,11 +23,11 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
|
|
|
23
23
|
if (initialTab === "pipeline" || initialTab === "local" || initialTab === "files") setTab(initialTab);
|
|
24
24
|
}, [initialTab]);
|
|
25
25
|
return (
|
|
26
|
-
<div className="space-y-
|
|
27
|
-
<header>
|
|
28
|
-
<div className="
|
|
29
|
-
<h1 className="
|
|
30
|
-
<p className="
|
|
26
|
+
<div className="space-y-5">
|
|
27
|
+
<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>
|
|
31
31
|
</header>
|
|
32
32
|
<Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as CaptureTab)} />
|
|
33
33
|
{tab === "files" ? <FilesPanel /> : null}
|
|
@@ -49,14 +49,14 @@ function FilesPanel() {
|
|
|
49
49
|
<div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
|
|
50
50
|
<Card>
|
|
51
51
|
<CardHeader>
|
|
52
|
-
<CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" />
|
|
53
|
-
<CardDescription>
|
|
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>
|
|
54
54
|
</CardHeader>
|
|
55
55
|
<CardContent>
|
|
56
|
-
<label className="flex min-h-
|
|
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">
|
|
57
57
|
<Upload className="h-7 w-7 text-primary" />
|
|
58
|
-
<span className="font-
|
|
59
|
-
<span className="text-sm text-muted-foreground">PDF,
|
|
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
60
|
<input type="file" multiple className="sr-only" onChange={(e) => e.target.files && upload.mutate(e.target.files)} />
|
|
61
61
|
</label>
|
|
62
62
|
{upload.data ? (
|
|
@@ -86,12 +86,12 @@ function LocalPanel() {
|
|
|
86
86
|
<div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
|
87
87
|
<Card>
|
|
88
88
|
<CardHeader>
|
|
89
|
-
<CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> Connect folder</CardTitle>
|
|
90
|
-
<CardDescription>
|
|
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>
|
|
91
91
|
</CardHeader>
|
|
92
92
|
<CardContent className="space-y-3">
|
|
93
|
-
<Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="
|
|
94
|
-
<Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect
|
|
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
95
|
{connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
|
|
96
96
|
</CardContent>
|
|
97
97
|
</Card>
|
|
@@ -110,7 +110,7 @@ function LocalPanel() {
|
|
|
110
110
|
</div>
|
|
111
111
|
)}
|
|
112
112
|
</DataPanel>
|
|
113
|
-
<DataPanel title="
|
|
113
|
+
<DataPanel title="Folder access" result={agent.data} className="xl:col-span-2">
|
|
114
114
|
{(data) => <StructuredView value={data} />}
|
|
115
115
|
</DataPanel>
|
|
116
116
|
</div>
|
|
@@ -123,8 +123,8 @@ function BrowserPanel() {
|
|
|
123
123
|
return (
|
|
124
124
|
<Card>
|
|
125
125
|
<CardHeader>
|
|
126
|
-
<CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" />
|
|
127
|
-
<CardDescription>
|
|
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>
|
|
128
128
|
</CardHeader>
|
|
129
129
|
<CardContent className="space-y-3">
|
|
130
130
|
<div className="flex flex-col gap-2 sm:flex-row">
|
|
@@ -142,16 +142,16 @@ function PipelinePanel() {
|
|
|
142
142
|
const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
|
|
143
143
|
return (
|
|
144
144
|
<div className="grid gap-4 xl:grid-cols-2">
|
|
145
|
-
<DataPanel title="
|
|
145
|
+
<DataPanel title="Processing status" result={index.data}>
|
|
146
146
|
{(data) => <StructuredView value={data} />}
|
|
147
147
|
</DataPanel>
|
|
148
|
-
<DataPanel title="
|
|
148
|
+
<DataPanel title="Brain growth" result={stats.data}>
|
|
149
149
|
{(data) => <StructuredView value={data} />}
|
|
150
150
|
</DataPanel>
|
|
151
151
|
<Card className="xl:col-span-2">
|
|
152
152
|
<CardHeader>
|
|
153
|
-
<CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" />
|
|
154
|
-
<CardDescription>
|
|
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>
|
|
155
155
|
</CardHeader>
|
|
156
156
|
<CardContent>
|
|
157
157
|
<ActionButton label="Rebuild retrieval index" action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { Boxes, Cpu, PackagePlus, Plug,
|
|
3
|
+
import { Boxes, CheckCircle2, Cpu, Download, PackagePlus, PlayCircle, Plug, ShieldAlert } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
|
-
import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
|
|
5
|
+
import { ActionButton, DataPanel, EmptyState, EntityList, OperationResult, StatGrid, StructuredView, Tabs, ValuePreview } from "@/components/primitives";
|
|
6
6
|
import { Badge } from "@/components/ui/badge";
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
8
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
9
9
|
import { Input } from "@/components/ui/input";
|
|
10
|
+
import { useAppStore } from "@/store/appStore";
|
|
10
11
|
import { asArray } from "@/lib/utils";
|
|
11
12
|
|
|
12
13
|
type LibraryTab = "models" | "skills" | "mcp" | "marketplace";
|
|
@@ -14,23 +15,25 @@ type LibraryTab = "models" | "skills" | "mcp" | "marketplace";
|
|
|
14
15
|
const tabs: Array<{ id: LibraryTab; label: string }> = [
|
|
15
16
|
{ id: "models", label: "Models" },
|
|
16
17
|
{ id: "skills", label: "Skills" },
|
|
17
|
-
{ id: "mcp", label: "
|
|
18
|
+
{ id: "mcp", label: "Connections" },
|
|
18
19
|
{ id: "marketplace", label: "Marketplace" },
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
export function LibraryPage({ initialTab }: { initialTab?: string }) {
|
|
23
|
+
const mode = useAppStore((state) => state.mode);
|
|
22
24
|
const [tab, setTab] = React.useState<LibraryTab>((initialTab as LibraryTab) || "models");
|
|
23
25
|
React.useEffect(() => {
|
|
24
26
|
if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as LibraryTab);
|
|
25
27
|
}, [initialTab]);
|
|
28
|
+
const visibleTabs = tabs.map((item) => item.id === "mcp" && mode !== "basic" ? { ...item, label: "MCP / tools" } : item);
|
|
26
29
|
return (
|
|
27
|
-
<div className="space-y-
|
|
28
|
-
<header>
|
|
29
|
-
<div className="
|
|
30
|
-
<h1 className="
|
|
31
|
-
<p className="
|
|
30
|
+
<div className="space-y-5">
|
|
31
|
+
<header className="page-hero">
|
|
32
|
+
<div className="page-kicker"><Boxes className="h-4 w-4" /> Library</div>
|
|
33
|
+
<h1 className="page-title">Choose what powers Lattice.</h1>
|
|
34
|
+
<p className="page-copy">Pick a private local model, add skills, and connect tools without learning runtime internals.</p>
|
|
32
35
|
</header>
|
|
33
|
-
<Tabs tabs={
|
|
36
|
+
<Tabs tabs={visibleTabs} value={tab} onChange={(id) => setTab(id as LibraryTab)} />
|
|
34
37
|
{tab === "models" ? <ModelsPanel /> : null}
|
|
35
38
|
{tab === "skills" ? <SkillsPanel /> : null}
|
|
36
39
|
{tab === "mcp" ? <McpPanel /> : null}
|
|
@@ -40,15 +43,115 @@ export function LibraryPage({ initialTab }: { initialTab?: string }) {
|
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
function ModelsPanel() {
|
|
46
|
+
const qc = useQueryClient();
|
|
47
|
+
const mode = useAppStore((state) => state.mode);
|
|
43
48
|
const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
49
|
+
const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
|
|
44
50
|
const emb = useQuery({ queryKey: ["embeddings"], queryFn: latticeApi.embeddingsStatus });
|
|
51
|
+
const [consent, setConsent] = React.useState(false);
|
|
52
|
+
const [activeModel, setActiveModel] = React.useState<string | null>(null);
|
|
53
|
+
const [progress, setProgress] = React.useState<Record<string, unknown>[]>([]);
|
|
54
|
+
const [lastResult, setLastResult] = React.useState<Record<string, unknown> | null>(null);
|
|
55
|
+
const [lastError, setLastError] = React.useState<Record<string, unknown> | null>(null);
|
|
56
|
+
const [busy, setBusy] = React.useState(false);
|
|
45
57
|
const catalog = [
|
|
46
58
|
...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.catalog),
|
|
47
59
|
...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.recommended),
|
|
48
60
|
];
|
|
61
|
+
const recommendationRows = asArray<Record<string, unknown>>(
|
|
62
|
+
((recs.data?.data as Record<string, unknown>)?.recommendations as Record<string, unknown> | undefined)?.models,
|
|
63
|
+
);
|
|
64
|
+
const recommendationById = new Map(recommendationRows.map((item) => [String(item.id), item]));
|
|
65
|
+
const loadedIds = asArray<string>((models.data?.data as Record<string, unknown> | undefined)?.loaded);
|
|
66
|
+
const currentId = String((models.data?.data as Record<string, unknown> | undefined)?.current || "");
|
|
67
|
+
const current = catalog.find((model) => loadedIds.includes(String(model.id)) || String(model.id) === currentId);
|
|
68
|
+
const topPick = (((recs.data?.data as Record<string, unknown> | undefined)?.recommendations as Record<string, unknown> | undefined)?.top_pick || null) as Record<string, unknown> | null;
|
|
69
|
+
const latestProgress = progress[progress.length - 1] || null;
|
|
70
|
+
|
|
71
|
+
const modelMessage = React.useCallback((message: unknown) => {
|
|
72
|
+
const text = String(message || "This model is not ready to load yet.");
|
|
73
|
+
if (mode !== "basic") return text;
|
|
74
|
+
return text
|
|
75
|
+
.replace(/gemma4_unified/gi, "this local model format")
|
|
76
|
+
.replace(/mlx[-_ ]?vlm|mlx[-_ ]?lm|local_mlx|\bmlx\b|\bgguf\b|\bollama\b|hugging face/gi, "local model support")
|
|
77
|
+
.replace(/runtime/gi, "model support")
|
|
78
|
+
.replace(/model_type/gi, "model format")
|
|
79
|
+
.replace(/this local model format local model support format/gi, "this local model format")
|
|
80
|
+
.replace(/local model support model support/gi, "local model support");
|
|
81
|
+
}, [mode]);
|
|
82
|
+
|
|
83
|
+
async function prepareModel(loadId: string, engine: string, allowDownload: boolean) {
|
|
84
|
+
setBusy(true);
|
|
85
|
+
setActiveModel(loadId);
|
|
86
|
+
setProgress([]);
|
|
87
|
+
setLastResult(null);
|
|
88
|
+
setLastError(null);
|
|
89
|
+
const result = await latticeApi.streamModelPrepare(
|
|
90
|
+
{ model: loadId, engine: engine || "local_mlx", allow_download: allowDownload },
|
|
91
|
+
{
|
|
92
|
+
onProgress: (event) => setProgress((items) => [...items.slice(-8), event]),
|
|
93
|
+
onDone: (event) => setLastResult(event),
|
|
94
|
+
onError: (event) => setLastError(event),
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
setBusy(false);
|
|
98
|
+
await qc.invalidateQueries({ queryKey: ["models"] });
|
|
99
|
+
await qc.invalidateQueries({ queryKey: ["modelRecommendations", "local_mlx"] });
|
|
100
|
+
if (!result.ok && !lastError) setLastError(result.data as Record<string, unknown>);
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
return (
|
|
50
104
|
<div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
|
|
51
|
-
<
|
|
105
|
+
<div className="space-y-4">
|
|
106
|
+
<DataPanel title="Guided model setup" description="Analyze this Mac, recommend a model, install only with consent, validate it, then load it." result={recs.data}>
|
|
107
|
+
{(data) => {
|
|
108
|
+
const recommendation = (data as Record<string, unknown>).recommendations as Record<string, unknown> | undefined;
|
|
109
|
+
const profile = (data as Record<string, unknown>).profile as Record<string, unknown> | undefined;
|
|
110
|
+
return (
|
|
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
|
+
]} />
|
|
118
|
+
<div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
|
|
119
|
+
{[
|
|
120
|
+
["Environment Analysis", true, Cpu],
|
|
121
|
+
["Recommended Models", Boolean(topPick || catalog.length), CheckCircle2],
|
|
122
|
+
["Install", Boolean(current || latestProgress?.stage === "engine"), PackagePlus],
|
|
123
|
+
["Download Progress", Boolean(current || latestProgress?.stage === "download"), Download],
|
|
124
|
+
["Validate", Boolean(current || latestProgress?.stage === "smoke_test"), ShieldAlert],
|
|
125
|
+
["Load / Ready", Boolean(current || lastResult), PlayCircle],
|
|
126
|
+
].map(([label, done, Icon]) => (
|
|
127
|
+
<div key={String(label)} className="rounded-lg border border-border bg-background/55 p-3">
|
|
128
|
+
{React.createElement(Icon as typeof Cpu, { className: "h-4 w-4 text-primary" })}
|
|
129
|
+
<div className="mt-2 text-sm font-medium">{String(label)}</div>
|
|
130
|
+
<Badge variant={done ? "success" : "muted"}>{done ? "ready" : "pending"}</Badge>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
<label className="flex items-start gap-2 rounded-lg border border-border bg-background/55 p-3 text-sm leading-6">
|
|
135
|
+
<input className="mt-1" type="checkbox" checked={consent} onChange={(event) => setConsent(event.target.checked)} />
|
|
136
|
+
<span>
|
|
137
|
+
Allow Lattice to install a missing local model component or download model files for this action.
|
|
138
|
+
</span>
|
|
139
|
+
</label>
|
|
140
|
+
{latestProgress ? (
|
|
141
|
+
<div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
|
|
142
|
+
<div className="font-medium">{String(latestProgress.message || "Preparing model")}</div>
|
|
143
|
+
<div className="mt-2 h-2 overflow-hidden rounded-full bg-muted">
|
|
144
|
+
<div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
|
|
145
|
+
</div>
|
|
146
|
+
{latestProgress.detail ? <div className="mt-2 text-xs text-muted-foreground">{String(latestProgress.detail)}</div> : null}
|
|
147
|
+
</div>
|
|
148
|
+
) : null}
|
|
149
|
+
{lastError ? <ModelRecovery error={lastError} /> : null}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}}
|
|
153
|
+
</DataPanel>
|
|
154
|
+
<DataPanel title="Recommended models" result={models.data}>
|
|
52
155
|
{(data) => (
|
|
53
156
|
<div className="grid gap-2">
|
|
54
157
|
{(catalog.length ? catalog : asArray<Record<string, unknown>>((data as Record<string, unknown>).loaded)).slice(0, 14).map((model, index) => {
|
|
@@ -56,34 +159,120 @@ function ModelsPanel() {
|
|
|
56
159
|
const loaded = asArray<string>((data as Record<string, unknown>).loaded).includes(id) || (data as Record<string, unknown>).current === id || model.state === "loaded";
|
|
57
160
|
const loadId = String(model.recommended_load_id || id);
|
|
58
161
|
const engine = String(model.recommended_engine || model.engine || "");
|
|
59
|
-
const
|
|
162
|
+
const recommendation = recommendationById.get(id) || recommendationById.get(loadId) || {};
|
|
163
|
+
const compatibility = (model.runtime_compatibility || recommendation.runtime_compatibility || {}) as Record<string, unknown>;
|
|
164
|
+
const fallbackAvailable = String(compatibility.status || "") === "fallback_available";
|
|
165
|
+
const unsupported = model.load_status === "unsupported" || compatibility.supported === false;
|
|
166
|
+
const downloadRequired = Boolean(model.download_required);
|
|
167
|
+
const loadAvailable = (Boolean(model.load_available) || loaded) && !unsupported;
|
|
60
168
|
const loadStatus = String(model.load_status || (loaded ? "loaded" : "unavailable"));
|
|
61
|
-
const unavailableReason =
|
|
169
|
+
const unavailableReason = modelMessage(model.unavailable_reason || "This model is not ready to load yet.");
|
|
170
|
+
const runtimeLabel = String(model.runtime_label || compatibility.preferred_runtime || engine || "local_mlx");
|
|
171
|
+
const actionLabel = String(compatibility.action || loadStatus.replace(/_/g, " "));
|
|
172
|
+
const badgeLabel = unsupported && mode === "basic" ? "needs attention" : unsupported ? actionLabel : loadStatus;
|
|
173
|
+
const canPrepare = loadAvailable || downloadRequired;
|
|
62
174
|
return (
|
|
63
|
-
<div key={id} className="
|
|
64
|
-
<div>
|
|
65
|
-
<div className="
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
<div key={id} className="grid gap-3 rounded-lg border border-border bg-background/55 p-4 md:grid-cols-[1fr_auto]">
|
|
176
|
+
<div className="min-w-0">
|
|
177
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
178
|
+
<div className="text-base font-semibold">{String(model.name || id)}</div>
|
|
179
|
+
{topPick?.id === id ? <Badge variant="success">recommended</Badge> : null}
|
|
180
|
+
</div>
|
|
181
|
+
<div className="mt-1 text-sm text-muted-foreground">
|
|
182
|
+
{mode === "basic"
|
|
183
|
+
? [
|
|
184
|
+
modelMessage(model.family || recommendation.family || "Local model"),
|
|
185
|
+
/mlx|gguf|ollama/i.test(String(model.size || recommendation.size || "")) ? "" : model.size || recommendation.size,
|
|
186
|
+
].filter(Boolean).map(String).join(" · ")
|
|
187
|
+
: [model.family || recommendation.family || "local", model.size || recommendation.size].filter(Boolean).map(String).join(" · ")}
|
|
188
|
+
</div>
|
|
189
|
+
{unsupported ? (
|
|
190
|
+
<div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
|
|
191
|
+
<div className="font-medium">{mode === "basic" ? "Needs attention before loading" : actionLabel}</div>
|
|
192
|
+
<div className="text-muted-foreground">{modelMessage(compatibility.user_message || unavailableReason)}</div>
|
|
193
|
+
</div>
|
|
194
|
+
) : fallbackAvailable ? (
|
|
195
|
+
<div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
|
|
196
|
+
<div className="font-medium">{mode === "basic" ? "Compatible loading path available" : "Runtime fallback available"}</div>
|
|
197
|
+
<div className="text-muted-foreground">{modelMessage(compatibility.user_message || "Lattice will try the compatible local runtime path before showing this model as unsupported.")}</div>
|
|
198
|
+
</div>
|
|
199
|
+
) : !loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
|
|
200
|
+
{mode !== "basic" ? (
|
|
201
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
202
|
+
{runtimeLabel} · {loadId}
|
|
203
|
+
</div>
|
|
204
|
+
) : null}
|
|
205
|
+
{unsupported || fallbackAvailable ? <AlternativeModels compatibility={compatibility} /> : null}
|
|
68
206
|
</div>
|
|
69
|
-
<div className="flex items-center gap-2">
|
|
70
|
-
<Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" :
|
|
71
|
-
|
|
72
|
-
label=
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
207
|
+
<div className="flex flex-wrap items-center gap-2 md:justify-end">
|
|
208
|
+
<Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : badgeLabel}</Badge>
|
|
209
|
+
{loaded ? (
|
|
210
|
+
<ActionButton label="Unload" action={() => latticeApi.unloadModel(loadId)} invalidate={["models"]} />
|
|
211
|
+
) : (
|
|
212
|
+
<Button
|
|
213
|
+
variant="outline"
|
|
214
|
+
disabled={busy || unsupported || !canPrepare || (downloadRequired && !consent)}
|
|
215
|
+
onClick={() => prepareModel(loadId, engine || "local_mlx", consent)}
|
|
216
|
+
>
|
|
217
|
+
{activeModel === loadId && busy ? "Preparing" : downloadRequired ? "Install & Load" : "Validate & Load"}
|
|
218
|
+
</Button>
|
|
219
|
+
)}
|
|
77
220
|
</div>
|
|
78
221
|
</div>
|
|
79
222
|
);
|
|
80
223
|
})}
|
|
81
224
|
</div>
|
|
82
225
|
)}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
226
|
+
</DataPanel>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="space-y-4">
|
|
229
|
+
<DataPanel title={mode === "basic" ? "Memory search support" : "Embedding provider"} result={emb.data}>
|
|
230
|
+
{(data) => mode === "basic" ? <ValuePreview value={(data as Record<string, unknown>).state || "ready"} /> : <StructuredView value={data} />}
|
|
231
|
+
</DataPanel>
|
|
232
|
+
<DataPanel title="Model validation" result={models.data}>
|
|
233
|
+
{(data) => {
|
|
234
|
+
const profiles = asArray<Record<string, unknown>>((data as Record<string, unknown>).compat_profiles);
|
|
235
|
+
return profiles.length ? (
|
|
236
|
+
<EntityList items={profiles.map((profile) => ({
|
|
237
|
+
...profile,
|
|
238
|
+
name: mode === "basic" ? profile.name || profile.display_name || "Loaded model" : profile.name || profile.display_name || profile.model_id,
|
|
239
|
+
status: profile.quality_status || profile.load_status || "checked",
|
|
240
|
+
}))} titleKey="name" metaKey="status" limit={6} />
|
|
241
|
+
) : (
|
|
242
|
+
<EmptyState title="No model checked yet" detail="Load a model to confirm it can answer before you start using it." />
|
|
243
|
+
);
|
|
244
|
+
}}
|
|
245
|
+
</DataPanel>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function AlternativeModels({ compatibility }: { compatibility: Record<string, unknown> }) {
|
|
252
|
+
const mode = useAppStore((state) => state.mode);
|
|
253
|
+
const alternatives = asArray<Record<string, unknown>>(compatibility.alternatives);
|
|
254
|
+
if (!alternatives.length) return null;
|
|
255
|
+
return (
|
|
256
|
+
<div className="mt-2 flex flex-wrap gap-1">
|
|
257
|
+
{alternatives.slice(0, 3).map((item) => (
|
|
258
|
+
<Badge key={String(item.id || item.name)} variant="muted">
|
|
259
|
+
{mode === "basic" && /mlx|gguf|ollama|lm studio/i.test(String(item.name || item.id)) ? "Compatible alternative" : String(item.name || item.id)}
|
|
260
|
+
</Badge>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function ModelRecovery({ error }: { error: Record<string, unknown> }) {
|
|
267
|
+
const guidance = asArray<string>(error.recovery_guidance);
|
|
268
|
+
return (
|
|
269
|
+
<div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
|
|
270
|
+
<div className="font-medium">{String(error.user_message || "Model setup needs attention.")}</div>
|
|
271
|
+
{guidance.length ? (
|
|
272
|
+
<ul className="mt-2 list-inside list-disc text-muted-foreground">
|
|
273
|
+
{guidance.slice(0, 3).map((item) => <li key={item}>{item}</li>)}
|
|
274
|
+
</ul>
|
|
275
|
+
) : null}
|
|
87
276
|
</div>
|
|
88
277
|
);
|
|
89
278
|
}
|
|
@@ -133,18 +322,19 @@ function SkillsPanel() {
|
|
|
133
322
|
}
|
|
134
323
|
|
|
135
324
|
function McpPanel() {
|
|
325
|
+
const mode = useAppStore((state) => state.mode);
|
|
136
326
|
const [query, setQuery] = React.useState("github");
|
|
137
327
|
const tools = useQuery({ queryKey: ["mcpTools"], queryFn: latticeApi.mcpTools });
|
|
138
328
|
const rec = useMutation({ mutationFn: () => latticeApi.mcpRecommend(query) });
|
|
139
329
|
return (
|
|
140
330
|
<div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
|
|
141
|
-
<DataPanel title="MCP tools" result={tools.data}>
|
|
331
|
+
<DataPanel title={mode === "basic" ? "Tool connections" : "MCP tools"} result={tools.data}>
|
|
142
332
|
{(data) => <EntityList items={(data as Record<string, unknown>).tools || (data as Record<string, unknown>).installed_mcps} titleKey="name" metaKey="status" />}
|
|
143
333
|
</DataPanel>
|
|
144
334
|
<Card>
|
|
145
335
|
<CardHeader>
|
|
146
336
|
<CardTitle className="flex items-center gap-2"><Plug className="h-4 w-4" /> Recommend connector</CardTitle>
|
|
147
|
-
<CardDescription>
|
|
337
|
+
<CardDescription>Describe what you want to connect and Lattice will suggest a connector.</CardDescription>
|
|
148
338
|
</CardHeader>
|
|
149
339
|
<CardContent className="space-y-3">
|
|
150
340
|
<div className="flex gap-2">
|
|
@@ -176,7 +366,7 @@ function MarketplacePanel() {
|
|
|
176
366
|
<Card className="xl:col-span-3">
|
|
177
367
|
<CardHeader>
|
|
178
368
|
<CardTitle className="flex items-center gap-2"><PackagePlus className="h-4 w-4" /> Template install</CardTitle>
|
|
179
|
-
<CardDescription>
|
|
369
|
+
<CardDescription>Start from a reusable workspace pattern.</CardDescription>
|
|
180
370
|
</CardHeader>
|
|
181
371
|
<CardContent>
|
|
182
372
|
{asArray<Record<string, unknown>>((templates.data?.data as Record<string, unknown>)?.templates).length ? (
|