ltcai 7.9.0 → 8.1.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 (107) hide show
  1. package/README.md +35 -49
  2. package/docs/CHANGELOG.md +35 -0
  3. package/docs/V3_FRONTEND.md +11 -5
  4. package/lattice_brain/__init__.py +1 -1
  5. package/lattice_brain/embeddings.py +9 -1
  6. package/lattice_brain/graph/projection.py +19 -5
  7. package/lattice_brain/graph/provenance.py +26 -4
  8. package/lattice_brain/graph/write_master.py +30 -3
  9. package/lattice_brain/runtime/multi_agent.py +1 -1
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/app_factory.py +13 -134
  12. package/latticeai/core/local_embeddings.py +8 -0
  13. package/latticeai/core/marketplace.py +1 -1
  14. package/latticeai/core/mcp_registry.py +140 -1
  15. package/latticeai/core/tool_registry.py +7 -0
  16. package/latticeai/core/workspace_os.py +1 -1
  17. package/latticeai/services/architecture_readiness.py +50 -6
  18. package/latticeai/services/model_runtime.py +36 -2
  19. package/latticeai/services/product_readiness.py +19 -17
  20. package/package.json +2 -2
  21. package/scripts/build_frontend_assets.mjs +11 -2
  22. package/src-tauri/Cargo.lock +1 -1
  23. package/src-tauri/Cargo.toml +1 -1
  24. package/src-tauri/tauri.conf.json +1 -1
  25. package/static/app/asset-manifest.json +11 -102
  26. package/static/app/assets/Act-BOO66G-c.js +1 -0
  27. package/static/app/assets/Brain-C6lEYiD7.js +321 -0
  28. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
  29. package/static/app/assets/Library-DK4FIp8a.js +1 -0
  30. package/static/app/assets/System-Bgs6Ql7x.js +1 -0
  31. package/static/app/assets/core-CwxXejkd.js +1 -2
  32. package/static/app/assets/index-Bvv79nre.js +16 -0
  33. package/static/app/assets/index-Dslqglia.css +2 -0
  34. package/static/app/assets/primitives-B70WOra0.js +1 -0
  35. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
  36. package/static/app/index.html +3 -10
  37. package/static/app/theme-boot.js +8 -0
  38. package/static/sw.js +1 -1
  39. package/frontend/index.html +0 -24
  40. package/frontend/openapi.json +0 -15425
  41. package/frontend/src/App.tsx +0 -243
  42. package/frontend/src/api/client.ts +0 -580
  43. package/frontend/src/api/openapi.ts +0 -17892
  44. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  45. package/frontend/src/components/FeedbackState.tsx +0 -45
  46. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  47. package/frontend/src/components/LivingBrain.tsx +0 -220
  48. package/frontend/src/components/ProductFlow.tsx +0 -171
  49. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  50. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  51. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  52. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  53. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  54. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  55. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  56. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  57. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  58. package/frontend/src/components/primitives.tsx +0 -392
  59. package/frontend/src/components/ui/badge.tsx +0 -27
  60. package/frontend/src/components/ui/button.tsx +0 -37
  61. package/frontend/src/components/ui/card.tsx +0 -22
  62. package/frontend/src/components/ui/input.tsx +0 -16
  63. package/frontend/src/components/ui/textarea.tsx +0 -16
  64. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  65. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  66. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  67. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  68. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  69. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  70. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  71. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  72. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  73. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  74. package/frontend/src/features/brain/brainData.ts +0 -246
  75. package/frontend/src/features/brain/graphLayout.ts +0 -37
  76. package/frontend/src/features/brain/types.ts +0 -150
  77. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  78. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  79. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  80. package/frontend/src/i18n.ts +0 -1303
  81. package/frontend/src/lib/utils.ts +0 -33
  82. package/frontend/src/main.tsx +0 -23
  83. package/frontend/src/pages/Act.tsx +0 -458
  84. package/frontend/src/pages/Ask.tsx +0 -14
  85. package/frontend/src/pages/Brain.tsx +0 -914
  86. package/frontend/src/pages/Capture.tsx +0 -258
  87. package/frontend/src/pages/Library.tsx +0 -486
  88. package/frontend/src/pages/System.tsx +0 -621
  89. package/frontend/src/routes.ts +0 -92
  90. package/frontend/src/store/appStore.ts +0 -94
  91. package/frontend/src/styles.css +0 -6579
  92. package/static/app/assets/Act-DOvf59ru.js +0 -2
  93. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  94. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  95. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  96. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  97. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  98. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  99. package/static/app/assets/System-D6t9jo9V.js +0 -2
  100. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  101. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  102. package/static/app/assets/index-C7g26IF6.css +0 -2
  103. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  104. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  105. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  106. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  107. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -1,486 +0,0 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Boxes, CheckCircle2, Cpu, Download, PackagePlus, PlayCircle, Plug, ShieldAlert } from "lucide-react";
4
- import { latticeApi } from "@/api/client";
5
- import { ActionButton, DataPanel, EmptyState, EntityList, OperationResult, StatGrid, StructuredView, Tabs, ValuePreview } from "@/components/primitives";
6
- import { Badge } from "@/components/ui/badge";
7
- import { Button } from "@/components/ui/button";
8
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
- import { Input } from "@/components/ui/input";
10
- import { useAppStore } from "@/store/appStore";
11
- import { asArray } from "@/lib/utils";
12
-
13
- type LibraryTab = "models" | "skills" | "mcp" | "marketplace";
14
-
15
- const tabs: Array<{ id: LibraryTab; label: string }> = [
16
- { id: "models", label: "Models" },
17
- { id: "skills", label: "Skills" },
18
- { id: "mcp", label: "Connections" },
19
- { id: "marketplace", label: "Marketplace" },
20
- ];
21
-
22
- export function LibraryPage({ initialTab }: { initialTab?: string }) {
23
- const mode = useAppStore((state) => state.mode);
24
- const [tab, setTab] = React.useState<LibraryTab>((initialTab as LibraryTab) || "models");
25
- React.useEffect(() => {
26
- if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as LibraryTab);
27
- }, [initialTab]);
28
- const visibleTabs = tabs.map((item) => item.id === "mcp" && mode !== "basic" ? { ...item, label: "MCP / tools" } : item);
29
- return (
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 your Brain's voice.</h1>
34
- <p className="page-copy">Start with a short local model recommendation. Your Brain and memories stay durable when you switch later.</p>
35
- </header>
36
- <Tabs tabs={visibleTabs} value={tab} onChange={(id) => setTab(id as LibraryTab)} />
37
- {tab === "models" ? <ModelsPanel /> : null}
38
- {tab === "skills" ? <SkillsPanel /> : null}
39
- {tab === "mcp" ? <McpPanel /> : null}
40
- {tab === "marketplace" ? <MarketplacePanel /> : null}
41
- </div>
42
- );
43
- }
44
-
45
- function ModelsPanel() {
46
- const qc = useQueryClient();
47
- const mode = useAppStore((state) => state.mode);
48
- const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
49
- const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
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);
57
- const catalog = [
58
- ...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.catalog),
59
- ...asArray<Record<string, unknown>>((models.data?.data as Record<string, unknown>)?.recommended),
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
-
103
- return (
104
- <div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
105
- <div className="space-y-4">
106
- <DataPanel title="Guided model setup" description="Lattice recommends a small set first, explains internet/download needs, and keeps every model download behind consent." 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
- <ModelRuntimeSummary
113
- profile={profile || {}}
114
- recommendation={recommendation || {}}
115
- current={current}
116
- currentId={currentId}
117
- latestProgress={latestProgress}
118
- lastResult={lastResult}
119
- onReload={() => {
120
- if (current) void prepareModel(String(current.recommended_load_id || current.id || currentId), String(current.recommended_engine || current.engine || "local_mlx"), consent);
121
- }}
122
- onUnload={() => {
123
- if (currentId) void latticeApi.unloadModel(currentId).then(() => qc.invalidateQueries({ queryKey: ["models"] }));
124
- }}
125
- />
126
- <div className="grid gap-2 md:grid-cols-3 xl:grid-cols-6">
127
- {[
128
- ["Environment Analysis", true, Cpu],
129
- ["Recommended Models", Boolean(topPick || catalog.length), CheckCircle2],
130
- ["Install", Boolean(current || latestProgress?.stage === "engine"), PackagePlus],
131
- ["Download Progress", Boolean(current || latestProgress?.stage === "download"), Download],
132
- ["Validate", Boolean(current || latestProgress?.stage === "smoke_test"), ShieldAlert],
133
- ["Load / Ready", Boolean(current || lastResult), PlayCircle],
134
- ].map(([label, done, Icon]) => (
135
- <div key={String(label)} className="rounded-lg border border-border bg-background/55 p-3">
136
- {React.createElement(Icon as typeof Cpu, { className: "h-4 w-4 text-primary" })}
137
- <div className="mt-2 text-sm font-medium">{String(label)}</div>
138
- <Badge variant={done ? "success" : "muted"}>{done ? "ready" : "pending"}</Badge>
139
- </div>
140
- ))}
141
- </div>
142
- <label className="flex items-start gap-2 rounded-lg border border-border bg-background/55 p-3 text-sm leading-6">
143
- <input className="mt-1" type="checkbox" checked={consent} onChange={(event) => setConsent(event.target.checked)} />
144
- <span>
145
- Allow Lattice to install a missing local model component or download model files for this action. Download uses the internet; running the model stays local.
146
- </span>
147
- </label>
148
- {latestProgress ? (
149
- <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
150
- <div className="font-medium">{String(latestProgress.message || "Preparing model")}</div>
151
- <div className="mt-2 h-2 overflow-hidden rounded-full bg-muted">
152
- <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
153
- </div>
154
- {latestProgress.detail ? <div className="mt-2 text-xs text-muted-foreground">{String(latestProgress.detail)}</div> : null}
155
- </div>
156
- ) : null}
157
- {lastError ? <ModelRecovery error={lastError} /> : null}
158
- </div>
159
- );
160
- }}
161
- </DataPanel>
162
- <DataPanel title={mode === "basic" ? "Recommended models" : "Recommended and advanced models"} result={models.data}>
163
- {(data) => (
164
- <div className="grid gap-2">
165
- {(catalog.length ? catalog : asArray<Record<string, unknown>>((data as Record<string, unknown>).loaded)).slice(0, mode === "basic" ? 3 : 14).map((model, index) => {
166
- const id = String(model.id || model.model_id || model.name || index);
167
- const loaded = asArray<string>((data as Record<string, unknown>).loaded).includes(id) || (data as Record<string, unknown>).current === id || model.state === "loaded";
168
- const loadId = String(model.recommended_load_id || id);
169
- const engine = String(model.recommended_engine || model.engine || "");
170
- const recommendation = recommendationById.get(id) || recommendationById.get(loadId) || {};
171
- const modelVerification = asRecord(model.verification);
172
- const recommendationVerification = asRecord(recommendation.verification);
173
- const modelHardware = asRecord(model.hardware);
174
- const recommendationHardware = asRecord(recommendation.hardware);
175
- const hardwareNote = modelHardware.notes || recommendationHardware.notes || (modelHardware.recommended_ram_gb ? `~${modelHardware.recommended_ram_gb}GB RAM rec` : "");
176
- const safetyNotes = model.safety_notes || recommendation.safety_notes;
177
- const licenseText = model.license || recommendation.license;
178
- const compatibility = (model.runtime_compatibility || recommendation.runtime_compatibility || {}) as Record<string, unknown>;
179
- const fallbackAvailable = String(compatibility.status || "") === "fallback_available";
180
- const unsupported = model.load_status === "unsupported" || compatibility.supported === false;
181
- const downloadRequired = Boolean(model.download_required);
182
- const loadAvailable = (Boolean(model.load_available) || loaded) && !unsupported;
183
- const loadStatus = String(model.load_status || (loaded ? "loaded" : "unavailable"));
184
- const unavailableReason = modelMessage(model.unavailable_reason || "This model is not ready to load yet.");
185
- const runtimeLabel = String(model.runtime_label || compatibility.preferred_runtime || engine || "local_mlx");
186
- const actionLabel = String(compatibility.action || loadStatus.replace(/_/g, " "));
187
- const badgeLabel = unsupported && mode === "basic" ? "needs attention" : unsupported ? actionLabel : loadStatus;
188
- const canPrepare = loadAvailable || downloadRequired;
189
- const downloadSize = model.download_size || model.estimated_size || recommendation.download_size || recommendation.estimated_size || model.size || recommendation.size;
190
- const maker = model.provider || recommendation.provider || model.organization || recommendation.organization;
191
- return (
192
- <div key={id} className="grid gap-3 rounded-lg border border-border bg-background/55 p-4 md:grid-cols-[1fr_auto]">
193
- <div className="min-w-0">
194
- <div className="flex flex-wrap items-center gap-2">
195
- <div className="text-base font-semibold">{String(model.name || id)}</div>
196
- {topPick?.id === id || model.recommended_default ? <Badge variant="success">recommended</Badge> : null}
197
- {String(model.modality || recommendation.modality || "").includes("multi") || String(model.modality || "") === "multimodal" ? <Badge variant="muted">multimodal</Badge> : null}
198
- {modelVerification.verified || recommendationVerification.verified ? <Badge variant="success" title="HF verified (config+tokenizer present)">✓ HF</Badge> : null}
199
- </div>
200
- <div className="mt-1 text-sm text-muted-foreground">
201
- {mode === "basic"
202
- ? [
203
- modelMessage(model.family || recommendation.family || "Local model"),
204
- /mlx|gguf|ollama/i.test(String(model.size || recommendation.size || "")) ? "" : model.size || recommendation.size,
205
- ].filter(Boolean).map(String).join(" · ")
206
- : [model.family || recommendation.family || "local", model.size || recommendation.size].filter(Boolean).map(String).join(" · ")}
207
- </div>
208
- {(model.hardware || recommendation.hardware) ? (
209
- <div className="mt-1 text-[11px] text-muted-foreground/80">
210
- {String(hardwareNote)}
211
- </div>
212
- ) : null}
213
- <div className="mt-2 flex flex-wrap gap-1 text-[11px] text-muted-foreground">
214
- <Badge variant="muted">{downloadRequired ? `Download: ${String(downloadSize || "required")}` : "No download needed now"}</Badge>
215
- <Badge variant="muted">{downloadRequired ? "Internet only during download" : "Runs locally when loaded"}</Badge>
216
- {maker ? <Badge variant="muted">{String(maker)}</Badge> : null}
217
- </div>
218
- {unsupported ? (
219
- <div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
220
- <div className="font-medium">{mode === "basic" ? "Needs attention before loading" : actionLabel}</div>
221
- <div className="text-muted-foreground">{modelMessage(compatibility.user_message || unavailableReason)}</div>
222
- </div>
223
- ) : fallbackAvailable ? (
224
- <div className="mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
225
- <div className="font-medium">{mode === "basic" ? "Compatible loading path available" : "Runtime fallback available"}</div>
226
- <div className="text-muted-foreground">{modelMessage(compatibility.user_message || "Lattice will try the compatible local runtime path before showing this model as unsupported.")}</div>
227
- </div>
228
- ) : !loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
229
- {mode !== "basic" ? (
230
- <div className="mt-2 space-y-1 text-xs text-muted-foreground">
231
- <div>
232
- {runtimeLabel} · {loadId}
233
- {model.load_strategy || recommendation.load_strategy ? ` · ${String(model.load_strategy || recommendation.load_strategy)}` : ""}
234
- {modelVerification.notes ? ` · ${String(modelVerification.notes).slice(0,60)}` : ""}
235
- </div>
236
- {safetyNotes || licenseText ? (
237
- <div>{[licenseText ? `License: ${String(licenseText)}` : "", safetyNotes ? String(safetyNotes) : ""].filter(Boolean).join(" · ")}</div>
238
- ) : null}
239
- </div>
240
- ) : null}
241
- {unsupported || fallbackAvailable ? <AlternativeModels compatibility={compatibility} /> : null}
242
- </div>
243
- <div className="flex flex-wrap items-center gap-2 md:justify-end">
244
- <Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : badgeLabel}</Badge>
245
- {loaded ? (
246
- <ActionButton label="Unload" action={() => latticeApi.unloadModel(loadId)} invalidate={["models"]} />
247
- ) : (
248
- <Button
249
- variant="outline"
250
- disabled={busy || unsupported || !canPrepare || (downloadRequired && !consent)}
251
- onClick={() => prepareModel(loadId, engine || "local_mlx", consent)}
252
- >
253
- {activeModel === loadId && busy ? "Preparing" : downloadRequired ? "Install & Load" : "Validate & Load"}
254
- </Button>
255
- )}
256
- </div>
257
- </div>
258
- );
259
- })}
260
- {mode === "basic" && catalog.length > 3 ? (
261
- <div className="rounded-lg border border-border bg-background/55 p-3 text-sm text-muted-foreground">
262
- Showing the safest short list. Switch to Advanced for the full registry, runtime details, licenses, and safety notes.
263
- </div>
264
- ) : null}
265
- </div>
266
- )}
267
- </DataPanel>
268
- </div>
269
- <div className="space-y-4">
270
- <DataPanel title={mode === "basic" ? "Memory search support" : "Embedding provider"} result={emb.data}>
271
- {(data) => mode === "basic" ? <ValuePreview value={(data as Record<string, unknown>).state || "ready"} /> : <StructuredView value={data} />}
272
- </DataPanel>
273
- <DataPanel title="Model validation" result={models.data}>
274
- {(data) => {
275
- const profiles = asArray<Record<string, unknown>>((data as Record<string, unknown>).compat_profiles);
276
- return profiles.length ? (
277
- <EntityList items={profiles.map((profile) => ({
278
- ...profile,
279
- name: mode === "basic" ? profile.name || profile.display_name || "Loaded model" : profile.name || profile.display_name || profile.model_id,
280
- status: profile.quality_status || profile.load_status || "checked",
281
- }))} titleKey="name" metaKey="status" limit={6} />
282
- ) : (
283
- <EmptyState title="No model checked yet" detail="Load a model to confirm it can answer before you start using it." />
284
- );
285
- }}
286
- </DataPanel>
287
- </div>
288
- </div>
289
- );
290
- }
291
-
292
- function AlternativeModels({ compatibility }: { compatibility: Record<string, unknown> }) {
293
- const mode = useAppStore((state) => state.mode);
294
- const alternatives = asArray<Record<string, unknown>>(compatibility.alternatives);
295
- if (!alternatives.length) return null;
296
- return (
297
- <div className="mt-2 flex flex-wrap gap-1">
298
- {alternatives.slice(0, 3).map((item) => (
299
- <Badge key={String(item.id || item.name)} variant="muted">
300
- {mode === "basic" && /mlx|gguf|ollama|lm studio/i.test(String(item.name || item.id)) ? "Compatible alternative" : String(item.name || item.id)}
301
- </Badge>
302
- ))}
303
- </div>
304
- );
305
- }
306
-
307
- function ModelRuntimeSummary({
308
- profile,
309
- recommendation,
310
- current,
311
- currentId,
312
- latestProgress,
313
- lastResult,
314
- onReload,
315
- onUnload,
316
- }: {
317
- profile: Record<string, unknown>;
318
- recommendation: Record<string, unknown>;
319
- current?: Record<string, unknown>;
320
- currentId: string;
321
- latestProgress: Record<string, unknown> | null;
322
- lastResult: Record<string, unknown> | null;
323
- onReload: () => void;
324
- onUnload: () => void;
325
- }) {
326
- const loadedName = String(current?.name || current?.id || currentId || "");
327
- const engine = String(current?.engine || current?.recommended_engine || latestProgress?.engine || "local_mlx");
328
- const cachePath = String(current?.local_path || current?.storage_location || recommendation.cache_path || recommendation.storage_location || "~/.cache/huggingface / ~/.latticeai/models");
329
- const progressStage = String(latestProgress?.stage || lastResult?.stage || (loadedName ? "ready" : "idle"));
330
- return (
331
- <div className="space-y-3">
332
- <StatGrid stats={[
333
- { label: "Computer", value: profile?.os ? `${String(profile.os)} ${String(profile.arch || "")}` : "detected" },
334
- { label: "Engine", value: engine },
335
- { label: "Loaded model", value: loadedName || "No local model loaded" },
336
- { label: "Runtime state", value: progressStage },
337
- ]} />
338
- <div className="rounded-lg border border-border bg-background/55 p-3 text-sm">
339
- <div className="flex flex-wrap items-start justify-between gap-3">
340
- <div>
341
- <div className="font-medium">{loadedName ? "Local model runtime is available" : "No model is loaded yet"}</div>
342
- <div className="mt-1 text-muted-foreground">Cache/storage: {cachePath}</div>
343
- </div>
344
- <div className="flex flex-wrap gap-2">
345
- <Button variant="outline" size="sm" disabled={!loadedName} onClick={onReload}>Reload</Button>
346
- <Button variant="outline" size="sm" disabled={!loadedName} onClick={onUnload}>Unload</Button>
347
- </div>
348
- </div>
349
- {latestProgress ? (
350
- <div className="mt-3">
351
- <div className="flex justify-between text-xs text-muted-foreground">
352
- <span>{String(latestProgress.message || "Preparing model")}</span>
353
- <span>{Number(latestProgress.percent || 0)}%</span>
354
- </div>
355
- <div className="mt-1 h-2 overflow-hidden rounded-full bg-muted">
356
- <div className="h-full bg-primary" style={{ width: `${Number(latestProgress.percent || 8)}%` }} />
357
- </div>
358
- </div>
359
- ) : null}
360
- </div>
361
- </div>
362
- );
363
- }
364
-
365
- function ModelRecovery({ error }: { error: Record<string, unknown> }) {
366
- const guidance = asArray<string>(error.recovery_guidance);
367
- return (
368
- <div className="rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-sm">
369
- <div className="font-medium">{String(error.user_message || "Model setup needs attention.")}</div>
370
- {guidance.length ? (
371
- <ul className="mt-2 list-inside list-disc text-muted-foreground">
372
- {guidance.slice(0, 3).map((item) => <li key={item}>{item}</li>)}
373
- </ul>
374
- ) : null}
375
- </div>
376
- );
377
- }
378
-
379
- function asRecord(value: unknown): Record<string, unknown> {
380
- return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
381
- }
382
-
383
- function SkillsPanel() {
384
- const qc = useQueryClient();
385
- const skills = useQuery({ queryKey: ["skills"], queryFn: latticeApi.skills });
386
- const market = useQuery({ queryKey: ["skillsMarketplace"], queryFn: latticeApi.skillsMarketplace });
387
- const install = useMutation({
388
- mutationFn: (skill: Record<string, unknown>) => latticeApi.skillInstall(String(skill.name || skill.id), String(skill.plugin || "")),
389
- onSuccess: () => qc.invalidateQueries({ queryKey: ["skills"] }),
390
- });
391
- return (
392
- <div className="grid gap-4 xl:grid-cols-2">
393
- <DataPanel title="Installed skills" result={skills.data}>
394
- {(data) => (
395
- <div className="grid gap-2">
396
- {asArray<Record<string, unknown>>((data as Record<string, unknown>).skills).map((skill) => (
397
- <div key={String(skill.name || skill.id)} className="flex items-center justify-between gap-3 rounded-md border border-border p-3">
398
- <div>
399
- <div className="font-medium">{String(skill.name || skill.id)}</div>
400
- <div className="text-sm text-muted-foreground">{String(skill.plugin || skill.description || "")}</div>
401
- </div>
402
- <ActionButton label={skill.enabled ? "Disable" : "Enable"} action={() => latticeApi.skillToggle(String(skill.name || skill.id), Boolean(skill.enabled))} invalidate={["skills"]} />
403
- </div>
404
- ))}
405
- </div>
406
- )}
407
- </DataPanel>
408
- <DataPanel title="Skill marketplace" result={market.data}>
409
- {(data) => (
410
- <div className="grid gap-2">
411
- {asArray<Record<string, unknown>>((data as Record<string, unknown>).skills).slice(0, 10).map((skill) => (
412
- <div key={String(skill.name || skill.id)} className="flex items-center justify-between gap-3 rounded-md border border-border p-3">
413
- <div>
414
- <div className="font-medium">{String(skill.name || skill.id)}</div>
415
- <div className="text-sm text-muted-foreground">{String(skill.description || skill.category || "")}</div>
416
- </div>
417
- <Button variant="outline" disabled={install.isPending} onClick={() => install.mutate(skill)}>Install</Button>
418
- </div>
419
- ))}
420
- </div>
421
- )}
422
- </DataPanel>
423
- </div>
424
- );
425
- }
426
-
427
- function McpPanel() {
428
- const mode = useAppStore((state) => state.mode);
429
- const [query, setQuery] = React.useState("github");
430
- const tools = useQuery({ queryKey: ["mcpTools"], queryFn: latticeApi.mcpTools });
431
- const rec = useMutation({ mutationFn: () => latticeApi.mcpRecommend(query) });
432
- return (
433
- <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">
434
- <DataPanel title={mode === "basic" ? "Tool connections" : "MCP tools"} result={tools.data}>
435
- {(data) => <EntityList items={(data as Record<string, unknown>).tools || (data as Record<string, unknown>).installed_mcps} titleKey="name" metaKey="status" />}
436
- </DataPanel>
437
- <Card>
438
- <CardHeader>
439
- <CardTitle className="flex items-center gap-2"><Plug className="h-4 w-4" /> Recommend connector</CardTitle>
440
- <CardDescription>Describe what you want to connect and Lattice will suggest a connector.</CardDescription>
441
- </CardHeader>
442
- <CardContent className="space-y-3">
443
- <div className="flex gap-2">
444
- <Input value={query} onChange={(e) => setQuery(e.target.value)} />
445
- <Button onClick={() => rec.mutate()} disabled={!query.trim() || rec.isPending}>Recommend</Button>
446
- </div>
447
- {rec.data ? <OperationResult result={rec.data} successLabel="Recommendation completed" /> : null}
448
- </CardContent>
449
- </Card>
450
- </div>
451
- );
452
- }
453
-
454
- function MarketplacePanel() {
455
- const templates = useQuery({ queryKey: ["templates"], queryFn: latticeApi.templates });
456
- const plugins = useQuery({ queryKey: ["plugins"], queryFn: latticeApi.pluginsRegistry });
457
- const dir = useQuery({ queryKey: ["pluginsDirectory"], queryFn: latticeApi.pluginsDirectory });
458
- return (
459
- <div className="grid gap-4 xl:grid-cols-3">
460
- <DataPanel title="Templates" result={templates.data}>
461
- {(data) => <EntityList items={(data as Record<string, unknown>).templates} titleKey="name" metaKey="kind" />}
462
- </DataPanel>
463
- <DataPanel title="Installed plugins" result={plugins.data}>
464
- {(data) => <EntityList items={(data as Record<string, unknown>).plugins} titleKey="name" metaKey="status" />}
465
- </DataPanel>
466
- <DataPanel title="Plugin directory" result={dir.data}>
467
- {(data) => <EntityList items={(data as Record<string, unknown>).plugins} titleKey="name" metaKey="category" />}
468
- </DataPanel>
469
- <Card className="xl:col-span-3">
470
- <CardHeader>
471
- <CardTitle className="flex items-center gap-2"><PackagePlus className="h-4 w-4" /> Template install</CardTitle>
472
- <CardDescription>Start from a reusable workspace pattern.</CardDescription>
473
- </CardHeader>
474
- <CardContent>
475
- {asArray<Record<string, unknown>>((templates.data?.data as Record<string, unknown>)?.templates).length ? (
476
- <div className="flex flex-wrap gap-2">
477
- {asArray<Record<string, unknown>>((templates.data?.data as Record<string, unknown>)?.templates).slice(0, 6).map((template) => (
478
- <ActionButton key={String(template.id || template.name)} label={`Install ${String(template.name || template.id)}`} action={() => latticeApi.installTemplate(template)} />
479
- ))}
480
- </div>
481
- ) : <EntityList items={[]} />}
482
- </CardContent>
483
- </Card>
484
- </div>
485
- );
486
- }