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
@@ -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: "Local folders" },
16
- { id: "browser", label: "Web capture" },
17
- { id: "pipeline", label: "Pipeline" },
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-4">
27
- <header>
28
- <div className="flex items-center gap-2 text-sm text-primary"><Upload className="h-4 w-4" /> One ingestion door</div>
29
- <h1 className="mt-2 text-3xl font-semibold">Capture</h1>
30
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Documents, folders, and URLs enter the brain through existing ingestion endpoints with provenance.</p>
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" /> Upload documents</CardTitle>
53
- <CardDescription>Multipart upload to `/upload/document`; accepted files are parsed and indexed by the backend.</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-44 cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-muted/30 p-5 text-center">
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-medium">Choose files</span>
59
- <span className="text-sm text-muted-foreground">PDF, DOCX, XLSX, PPTX, TXT, MD, CSV according to backend policy.</span>
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>The click is explicit consent; the backend still enforces its permission workflow.</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="/Users/me/Documents/project" />
94
- <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect and watch</Button>
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="Local runtime probe" result={agent.data} className="xl:col-span-2">
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" /> URL capture</CardTitle>
127
- <CardDescription>Fetches a URL locally through `/api/browser/read-url` and ingests the content with provenance.</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="Index pipeline" result={index.data}>
145
+ <DataPanel title="Processing status" result={index.data}>
146
146
  {(data) => <StructuredView value={data} />}
147
147
  </DataPanel>
148
- <DataPanel title="Graph totals" result={stats.data}>
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" /> Rebuild controls</CardTitle>
154
- <CardDescription>Rebuild calls the existing index endpoint. No background work is implied unless the API accepts it.</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, Puzzle } from "lucide-react";
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: "MCP" },
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-4">
28
- <header>
29
- <div className="flex items-center gap-2 text-sm text-primary"><Boxes className="h-4 w-4" /> Replaceable runtime assets</div>
30
- <h1 className="mt-2 text-3xl font-semibold">Library</h1>
31
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Models, skills, MCP servers, plugins, and templates are managed by local backend registries.</p>
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={tabs} value={tab} onChange={(id) => setTab(id as LibraryTab)} />
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
- <DataPanel title="Model catalog" result={models.data}>
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 loadAvailable = Boolean(model.load_available) || loaded;
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 = String(model.unavailable_reason || "Unavailable until the backend reports a local model/runtime ready.");
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="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background p-3">
64
- <div>
65
- <div className="font-medium">{String(model.name || id)}</div>
66
- <div className="text-sm text-muted-foreground">{String(model.family || model.engine || model.recommended_engine || "local")}</div>
67
- {!loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
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" : loadStatus}</Badge>
71
- <ActionButton
72
- label={loaded ? "Unload" : "Load"}
73
- action={() => loaded ? latticeApi.unloadModel(loadId) : latticeApi.loadModel(loadId, engine, false)}
74
- invalidate={["models"]}
75
- disabled={!loaded && !loadAvailable}
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
- </DataPanel>
84
- <DataPanel title="Embedding provider" result={emb.data}>
85
- {(data) => <StructuredView value={data} />}
86
- </DataPanel>
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>Calls `/mcp/recommend`; returned installability depends on available connectors.</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>Install controls are enabled only for template records returned by the backend.</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 ? (