ltcai 4.4.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +46 -18
  2. package/docs/CHANGELOG.md +85 -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_DIGITAL_BRAIN_RECOVERY.md +16 -16
  19. package/docs/architecture.md +8 -4
  20. package/frontend/src/App.tsx +152 -91
  21. package/frontend/src/api/client.ts +83 -1
  22. package/frontend/src/components/FirstRunGuide.tsx +99 -0
  23. package/frontend/src/components/primitives.tsx +131 -25
  24. package/frontend/src/components/ui/badge.tsx +2 -2
  25. package/frontend/src/components/ui/button.tsx +7 -7
  26. package/frontend/src/components/ui/card.tsx +5 -5
  27. package/frontend/src/components/ui/input.tsx +1 -1
  28. package/frontend/src/components/ui/textarea.tsx +1 -1
  29. package/frontend/src/pages/Act.tsx +58 -28
  30. package/frontend/src/pages/Ask.tsx +51 -19
  31. package/frontend/src/pages/Brain.tsx +60 -42
  32. package/frontend/src/pages/Capture.tsx +24 -24
  33. package/frontend/src/pages/Library.tsx +222 -32
  34. package/frontend/src/pages/System.tsx +56 -34
  35. package/frontend/src/routes.ts +15 -13
  36. package/frontend/src/store/appStore.ts +8 -1
  37. package/frontend/src/styles.css +666 -36
  38. package/lattice_brain/__init__.py +1 -1
  39. package/lattice_brain/runtime/multi_agent.py +1 -1
  40. package/latticeai/__init__.py +1 -1
  41. package/latticeai/api/models.py +107 -18
  42. package/latticeai/core/marketplace.py +1 -1
  43. package/latticeai/core/model_compat.py +250 -0
  44. package/latticeai/core/workspace_os.py +1 -1
  45. package/latticeai/models/router.py +136 -32
  46. package/latticeai/services/model_catalog.py +2 -2
  47. package/latticeai/services/model_recommendation.py +8 -1
  48. package/latticeai/services/model_runtime.py +18 -3
  49. package/package.json +1 -1
  50. package/scripts/build_frontend_assets.mjs +12 -1
  51. package/src-tauri/Cargo.lock +1 -1
  52. package/src-tauri/Cargo.toml +1 -1
  53. package/src-tauri/tauri.conf.json +1 -1
  54. package/static/app/asset-manifest.json +5 -5
  55. package/static/app/assets/index-3G8qcrIS.js +336 -0
  56. package/static/app/assets/index-3G8qcrIS.js.map +1 -0
  57. package/static/app/assets/index-C0wYZp7k.css +2 -0
  58. package/static/app/index.html +2 -2
  59. package/static/app/assets/index-CHHal8Zl.css +0 -2
  60. package/static/app/assets/index-pdzil9ac.js +0 -333
  61. package/static/app/assets/index-pdzil9ac.js.map +0 -1
@@ -77,6 +77,21 @@ function workspaceHeaders(): Record<string, string> {
77
77
  return workspaceId ? { "X-Workspace-Id": workspaceId } : {};
78
78
  }
79
79
 
80
+ function friendlyError(error: unknown, fallback: string) {
81
+ if (!error) return fallback;
82
+ const record = typeof error === "object" && error !== null ? error as Record<string, unknown> : null;
83
+ const detail = record?.detail;
84
+ if (typeof detail === "string") return detail;
85
+ const detailRecord = typeof detail === "object" && detail !== null ? detail as Record<string, unknown> : null;
86
+ if (detailRecord) {
87
+ const message = detailRecord.user_message || detailRecord.reason || detailRecord.action || detailRecord.status;
88
+ if (message) return String(message);
89
+ }
90
+ const message = record?.message || record?.error;
91
+ if (message) return String(message);
92
+ return fallback;
93
+ }
94
+
80
95
  async function apiJson<T>(
81
96
  method: HttpMethod,
82
97
  path: string,
@@ -108,7 +123,7 @@ async function apiJson<T>(
108
123
  status: response.status,
109
124
  data: emptyFor(opts.shape),
110
125
  source: "unavailable",
111
- error: error ? JSON.stringify(error) : response.statusText,
126
+ error: friendlyError(error, response.statusText),
112
127
  };
113
128
  } catch (err) {
114
129
  return {
@@ -163,6 +178,69 @@ export type ChatEventHandlers = {
163
178
  signal?: AbortSignal;
164
179
  };
165
180
 
181
+ export type ModelPrepareHandlers = {
182
+ onProgress?: (data: Record<string, unknown>) => void;
183
+ onDone?: (data: Record<string, unknown>) => void;
184
+ onError?: (data: Record<string, unknown>) => void;
185
+ signal?: AbortSignal;
186
+ };
187
+
188
+ async function streamModelPrepare(
189
+ body: { model: string; engine?: string; allow_download?: boolean },
190
+ handlers: ModelPrepareHandlers = {},
191
+ ) {
192
+ const base = await apiBase();
193
+ const res = await fetch(`${base}/engines/prepare-model/stream`, {
194
+ method: "POST",
195
+ credentials: "same-origin",
196
+ signal: handlers.signal,
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ Accept: "text/event-stream",
200
+ ...workspaceHeaders(),
201
+ } satisfies HeadersInit,
202
+ body: JSON.stringify({ engine: null, allow_download: false, ...body }),
203
+ });
204
+ if (!res.ok || !res.body || !(res.headers.get("content-type") || "").includes("text/event-stream")) {
205
+ const payload = await res.json().catch(() => null);
206
+ const detail = payload?.detail && typeof payload.detail === "object" ? payload.detail : payload;
207
+ const message = friendlyError(payload, res.statusText);
208
+ handlers.onError?.({ status: "error", user_message: message, ...(detail || {}) });
209
+ return { source: "live" as const, ok: false, status: res.status, data: detail || {}, error: message };
210
+ }
211
+ const reader = res.body.getReader();
212
+ const decoder = new TextDecoder();
213
+ let buffer = "";
214
+ let eventName = "message";
215
+ let finalData: Record<string, unknown> = {};
216
+ for (;;) {
217
+ const { done, value } = await reader.read();
218
+ if (done) break;
219
+ buffer += decoder.decode(value, { stream: true });
220
+ const parts = buffer.split("\n\n");
221
+ buffer = parts.pop() || "";
222
+ for (const part of parts) {
223
+ const lines = part.split("\n");
224
+ eventName = lines.find((item) => item.startsWith("event:"))?.slice(6).trim() || "message";
225
+ const dataLine = lines.find((item) => item.startsWith("data:"));
226
+ if (!dataLine) continue;
227
+ const raw = dataLine.slice(5).trim();
228
+ const data = raw ? JSON.parse(raw) as Record<string, unknown> : {};
229
+ if (eventName === "progress") handlers.onProgress?.(data);
230
+ if (eventName === "error") {
231
+ const detail = typeof data.detail === "object" && data.detail !== null ? data.detail as Record<string, unknown> : data;
232
+ handlers.onError?.(detail);
233
+ return { source: "live" as const, ok: false, status: Number(data.status_code || 500), data: detail, error: friendlyError({ detail }, "Model setup failed") };
234
+ }
235
+ if (eventName === "done") {
236
+ finalData = data;
237
+ handlers.onDone?.(data);
238
+ }
239
+ }
240
+ }
241
+ return { source: "live" as const, ok: true, status: 200, data: finalData };
242
+ }
243
+
166
244
  async function streamChat(body: Record<string, unknown>, handlers: ChatEventHandlers = {}) {
167
245
  const base = await apiBase();
168
246
  const res = await fetch(`${base}/chat`, {
@@ -263,6 +341,10 @@ export const latticeApi = {
263
341
  connectFolder: (path: string) => post("/knowledge-graph/local/index", { path, approved: true, watch_enabled: true, consent: { approved: true, source: "desktop-spa" } }, {}),
264
342
  localWatchStop: (source_id: string) => post("/knowledge-graph/local/watch/stop", { source_id }, {}),
265
343
  models: () => get("/models", { catalog: [], loaded: [], recommended: [] }),
344
+ modelRecommendations: (engine = "local_mlx") => get("/models/recommendations", { profile: {}, recommendations: { models: [], families: [], counts: {} } }, { engine }),
345
+ installEngine: (engine: string) => post("/engines/install", { engine }, {}),
346
+ prepareModel: (model: string, engine?: string, allow_download = false) => post("/engines/prepare-model", { model, engine: engine || null, allow_download }, {}),
347
+ streamModelPrepare,
266
348
  loadModel: (model_id: string, engine?: string, allow_download = false) => post("/models/load", { model_id, engine: engine || null, allow_download }, {}),
267
349
  unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
268
350
  embeddingsStatus: () => get("/api/embeddings/status", {}),
@@ -0,0 +1,99 @@
1
+ import * as React from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { ArrowRight, CheckCircle2, Cpu, Download, Layers3, Library, PlayCircle, SlidersHorizontal, UserCircle, Users } from "lucide-react";
4
+ import { latticeApi } from "@/api/client";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { useAppStore } from "@/store/appStore";
8
+ import { go } from "@/routes";
9
+ import { asArray } from "@/lib/utils";
10
+
11
+ function readDismissed() {
12
+ try {
13
+ return localStorage.getItem("lattice.onboarding.dismissed") === "true";
14
+ } catch {}
15
+ return false;
16
+ }
17
+
18
+ export function FirstRunGuide() {
19
+ const [dismissed, setDismissed] = React.useState(readDismissed);
20
+ const mode = useAppStore((state) => state.mode);
21
+ const profile = useQuery({ queryKey: ["profile"], queryFn: latticeApi.profile });
22
+ const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
23
+ const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
24
+ const recs = useQuery({ queryKey: ["modelRecommendations", "local_mlx"], queryFn: () => latticeApi.modelRecommendations("local_mlx") });
25
+ if (dismissed) return null;
26
+
27
+ const profileData = (profile.data?.data || {}) as Record<string, unknown>;
28
+ const workspaceData = (workspace.data?.data || {}) as Record<string, unknown>;
29
+ const registry = (workspaceData.workspace_registry || {}) as Record<string, unknown>;
30
+ const modelData = (models.data?.data || {}) as Record<string, unknown>;
31
+ const recommendationData = ((recs.data?.data as Record<string, unknown> | undefined)?.recommendations || {}) as Record<string, unknown>;
32
+ const currentModel = String(modelData.current || "");
33
+ const loadedModels = asArray(modelData.loaded);
34
+ const topPick = recommendationData.top_pick as Record<string, unknown> | undefined;
35
+ const compatProfiles = asArray<Record<string, unknown>>(modelData.compat_profiles);
36
+ const readyProfile = compatProfiles.some((item) => item.chat_compatible || item.quality_status === "ok" || item.quality_status === "degraded");
37
+
38
+ const steps = [
39
+ { label: "Make it yours", done: Boolean(profileData.email), icon: UserCircle, action: "account", detail: "Sign in or keep a local profile." },
40
+ { label: "Choose a space", done: Boolean(registry.active_workspace || workspaceData.active_workspace), icon: Users, action: "workspace-admin", detail: "Decide where memories belong." },
41
+ { label: "Meet your Mac", done: recs.isSuccess, icon: Cpu, action: "models", detail: "Let Lattice inspect what can run locally." },
42
+ { label: "Pick a brain", done: Boolean(topPick || currentModel), icon: Library, action: "models", detail: "Use the recommended local model." },
43
+ { label: "Install locally", done: Boolean(currentModel || loadedModels.length), icon: Download, action: "models", detail: "Download only with explicit consent." },
44
+ { label: "Try a question", done: Boolean(readyProfile || currentModel || loadedModels.length), icon: PlayCircle, action: "chat", detail: "Confirm the model can answer." },
45
+ { label: "Set the pace", done: Boolean(mode), icon: SlidersHorizontal, action: "settings", detail: "Stay Calm or switch deeper." },
46
+ { label: "Explore memory", done: true, icon: Layers3, action: "knowledge-graph", detail: "Open the living map." },
47
+ ];
48
+ const completed = steps.filter((step) => step.done).length;
49
+ const nextStep = steps.find((step) => !step.done) || steps[steps.length - 1];
50
+ const progress = Math.round((completed / steps.length) * 100);
51
+
52
+ return (
53
+ <section className="arrival-panel" aria-label="First 10 minutes">
54
+ <div className="arrival-copy">
55
+ <div className="page-kicker"><CheckCircle2 className="h-4 w-4" /> First 10 minutes</div>
56
+ <h2>Build your Digital Brain without guessing.</h2>
57
+ <p>
58
+ Start with a space, let Lattice recommend a private local model, then add the first pieces of knowledge.
59
+ Every step keeps the next action visible.
60
+ </p>
61
+ <div className="arrival-actions">
62
+ <Button onClick={() => go(nextStep.action)}>{nextStep.done ? "Open memory map" : `Continue: ${nextStep.label}`}</Button>
63
+ <Button variant="outline" onClick={() => go("models")}>Set up model</Button>
64
+ <Button variant="ghost" onClick={() => {
65
+ try { localStorage.setItem("lattice.onboarding.dismissed", "true"); } catch {}
66
+ setDismissed(true);
67
+ }}>
68
+ Hide
69
+ </Button>
70
+ </div>
71
+ </div>
72
+ <div className="journey-panel">
73
+ <div className="journey-head">
74
+ <div>
75
+ <div className="text-sm font-semibold">{completed} of {steps.length} ready</div>
76
+ <div className="text-xs text-muted-foreground">{mode === "basic" ? "Calm mode" : `${mode} mode`}</div>
77
+ </div>
78
+ <Badge variant={progress === 100 ? "success" : "warning"}>{progress}%</Badge>
79
+ </div>
80
+ <div className="journey-progress"><span style={{ width: `${progress}%` }} /></div>
81
+ <div className="journey-steps">
82
+ {steps.map((step) => {
83
+ const Icon = step.icon;
84
+ return (
85
+ <button key={step.label} onClick={() => go(step.action)} className="journey-step">
86
+ <span className="journey-icon"><Icon className="h-4 w-4" /></span>
87
+ <span className="min-w-0">
88
+ <span className="block truncate text-sm font-semibold">{step.label}</span>
89
+ <span className="block truncate text-xs text-muted-foreground">{step.detail}</span>
90
+ </span>
91
+ <ArrowRight className="h-3.5 w-3.5 text-muted-foreground" />
92
+ </button>
93
+ );
94
+ })}
95
+ </div>
96
+ </div>
97
+ </section>
98
+ );
99
+ }
@@ -1,24 +1,28 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQueryClient } from "@tanstack/react-query";
3
- import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react";
4
- import { ApiResult } from "@/api/client";
3
+ import { AlertCircle, CheckCircle2, Loader2, LockKeyhole, Sparkles } from "lucide-react";
4
+ import type { ApiResult } from "@/api/client";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
+ import { useAppStore } from "@/store/appStore";
8
9
  import { cn, asArray, fmtNumber, shortId, titleize } from "@/lib/utils";
9
10
 
10
11
  export function SourceBadge({ result }: { result?: Pick<ApiResult, "source" | "ok" | "status"> }) {
12
+ const mode = useAppStore((state) => state.mode);
11
13
  if (!result) return <Badge variant="muted">not loaded</Badge>;
12
- if (result.source === "live" && result.ok) return <Badge variant="success">live API</Badge>;
13
- return <Badge variant="warning">unavailable</Badge>;
14
+ if (result.source === "live" && result.ok) return <Badge variant="success">{mode === "basic" ? "ready" : "connected"}</Badge>;
15
+ return <Badge variant="warning">{mode === "basic" ? "needs setup" : "unavailable"}</Badge>;
14
16
  }
15
17
 
16
18
  export function EmptyState({ title = "Unavailable", detail }: { title?: string; detail?: React.ReactNode }) {
17
19
  return (
18
- <div className="flex min-h-28 flex-col items-center justify-center gap-2 rounded-md border border-dashed border-border bg-muted/30 p-5 text-center text-sm text-muted-foreground">
19
- <AlertCircle className="h-5 w-5" />
20
- <div className="font-medium text-foreground">{title}</div>
21
- {detail ? <div>{detail}</div> : null}
20
+ <div className="flex min-h-36 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border bg-muted/24 p-6 text-center text-sm text-muted-foreground">
21
+ <div className="grid h-10 w-10 place-items-center rounded-md border border-border bg-card">
22
+ <Sparkles className="h-5 w-5 text-primary" />
23
+ </div>
24
+ <div className="text-base font-semibold text-foreground">{title}</div>
25
+ {detail ? <div className="max-w-md leading-6">{detail}</div> : null}
22
26
  </div>
23
27
  );
24
28
  }
@@ -36,8 +40,9 @@ export function DataPanel<T>({
36
40
  children: (data: T) => React.ReactNode;
37
41
  className?: string;
38
42
  }) {
43
+ const mode = useAppStore((state) => state.mode);
39
44
  return (
40
- <Card className={className}>
45
+ <Card className={cn("overflow-hidden", className)}>
41
46
  <CardHeader className="flex-row items-start justify-between gap-3">
42
47
  <div>
43
48
  <CardTitle>{title}</CardTitle>
@@ -46,7 +51,9 @@ export function DataPanel<T>({
46
51
  <SourceBadge result={result} />
47
52
  </CardHeader>
48
53
  <CardContent>
49
- {result?.ok ? children(result.data) : <EmptyState detail={result?.error || "The backend did not return this capability."} />}
54
+ {result?.ok ? children(result.data) : (
55
+ <EmptyState detail={mode === "basic" ? "This area needs setup or is not available yet." : result?.error || "This capability is not reporting right now."} />
56
+ )}
50
57
  </CardContent>
51
58
  </Card>
52
59
  );
@@ -71,10 +78,10 @@ export function StatGrid({ stats }: { stats: Array<{ label: string; value: unkno
71
78
  return (
72
79
  <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
73
80
  {stats.map((stat) => (
74
- <div key={stat.label} className="rounded-md border border-border bg-background p-3">
75
- <div className="text-xs uppercase tracking-wide text-muted-foreground">{stat.label}</div>
76
- <div className="mt-1 text-2xl font-semibold">{typeof stat.value === "number" ? fmtNumber(stat.value) : String(stat.value ?? "-")}</div>
77
- {stat.hint ? <div className="mt-1 text-xs text-muted-foreground">{stat.hint}</div> : null}
81
+ <div key={stat.label} className="rounded-lg border border-border bg-background/55 p-4">
82
+ <div className="text-xs uppercase text-muted-foreground">{stat.label}</div>
83
+ <div className="mt-2 text-2xl font-semibold leading-tight">{typeof stat.value === "number" ? fmtNumber(stat.value) : String(stat.value ?? "-")}</div>
84
+ {stat.hint ? <div className="mt-2 text-xs leading-5 text-muted-foreground">{stat.hint}</div> : null}
78
85
  </div>
79
86
  ))}
80
87
  </div>
@@ -92,6 +99,32 @@ function scalarText(value: unknown) {
92
99
  return String(value);
93
100
  }
94
101
 
102
+ const BASIC_HIDDEN_KEY = /(^id$|_id$|token|secret|passphrase|fingerprint|public_key|private_key|dsn|schema|endpoint|base_url|localhost|127\.0\.0\.1|stack|trace|raw|runtime|engine|module|port|host|api|internal)/i;
103
+
104
+ function hideInBasic(key: string) {
105
+ return BASIC_HIDDEN_KEY.test(key);
106
+ }
107
+
108
+ function humanText(value: unknown) {
109
+ const text = scalarText(value);
110
+ if (text === "-") return text;
111
+ if (text.includes("/") || text.includes("@") || /\.[a-z0-9]{2,5}$/i.test(text)) return text;
112
+ return titleize(text.replace(/^agent:/i, "").replace(/^tool:/i, ""));
113
+ }
114
+
115
+ function firstRecordList(value: Record<string, unknown>) {
116
+ const preferred = [
117
+ "documents", "sources", "items", "agents", "workflows", "runs", "events",
118
+ "permissions", "models", "peers", "invitations", "roles", "policies",
119
+ "hooks", "tools", "templates", "plugins", "recent_events",
120
+ ];
121
+ for (const key of preferred) {
122
+ const rows = asArray<Record<string, unknown>>(value[key]);
123
+ if (rows.length) return rows;
124
+ }
125
+ return [];
126
+ }
127
+
95
128
  export function ValuePreview({ value }: { value: unknown }) {
96
129
  if (typeof value === "boolean") {
97
130
  return <Badge variant={value ? "success" : "muted"}>{value ? "enabled" : "disabled"}</Badge>;
@@ -119,7 +152,10 @@ export function ValuePreview({ value }: { value: unknown }) {
119
152
  }
120
153
 
121
154
  export function KeyValueList({ data, limit = 8 }: { data: Record<string, unknown>; limit?: number }) {
122
- const rows = Object.entries(data || {}).slice(0, limit);
155
+ const mode = useAppStore((state) => state.mode);
156
+ const rows = Object.entries(data || {})
157
+ .filter(([key]) => mode !== "basic" || !hideInBasic(key))
158
+ .slice(0, limit);
123
159
  if (!rows.length) return <EmptyState title="No values" />;
124
160
  return (
125
161
  <div className="divide-y divide-border rounded-md border border-border">
@@ -144,8 +180,10 @@ export function StructuredView({
144
180
  metaKey?: string;
145
181
  limit?: number;
146
182
  }) {
183
+ const mode = useAppStore((state) => state.mode);
184
+ if (mode === "basic") return <FriendlySummary value={value} titleKey={titleKey} metaKey={metaKey} limit={limit} />;
147
185
  if (Array.isArray(value)) {
148
- if (!value.length) return <EmptyState title="No records" detail="The API returned an empty collection." />;
186
+ if (!value.length) return <EmptyState title="Nothing here yet" detail="New items will appear here when Lattice has something to show." />;
149
187
  if (value.every((item) => isRecord(item))) {
150
188
  return <EntityList items={value} titleKey={titleKey} metaKey={metaKey} limit={limit} />;
151
189
  }
@@ -164,6 +202,46 @@ export function StructuredView({
164
202
  );
165
203
  }
166
204
 
205
+ export function FriendlySummary({
206
+ value,
207
+ titleKey = "title",
208
+ metaKey = "status",
209
+ limit = 6,
210
+ }: {
211
+ value: unknown;
212
+ titleKey?: string;
213
+ metaKey?: string;
214
+ limit?: number;
215
+ }) {
216
+ if (Array.isArray(value)) {
217
+ if (!value.length) return <EmptyState title="Nothing here yet" detail="New items will appear here when Lattice has something to show." />;
218
+ if (value.every((item) => isRecord(item))) {
219
+ return <EntityList items={value} titleKey={titleKey} metaKey={metaKey} limit={limit} />;
220
+ }
221
+ return (
222
+ <div className="flex flex-wrap gap-1 rounded-md border border-border bg-background/55 p-3">
223
+ {value.slice(0, limit).map((item, index) => <Badge key={`${String(item)}-${index}`} variant="muted">{humanText(item)}</Badge>)}
224
+ {value.length > limit ? <Badge variant="muted">+{value.length - limit}</Badge> : null}
225
+ </div>
226
+ );
227
+ }
228
+ if (isRecord(value)) {
229
+ const list = firstRecordList(value);
230
+ if (list.length) return <EntityList items={list} titleKey={titleKey} metaKey={metaKey} limit={limit} />;
231
+ const friendly = Object.fromEntries(
232
+ Object.entries(value)
233
+ .filter(([key]) => !hideInBasic(key))
234
+ .map(([key, item]) => [key, Array.isArray(item) ? `${fmtNumber(item.length)} items` : isRecord(item) ? "available" : item]),
235
+ );
236
+ return <KeyValueList data={friendly} limit={limit} />;
237
+ }
238
+ return (
239
+ <div className="rounded-md border border-border bg-background/55 p-3 text-sm">
240
+ {humanText(value)}
241
+ </div>
242
+ );
243
+ }
244
+
167
245
  export function OperationResult({
168
246
  result,
169
247
  successLabel = "Request completed",
@@ -171,6 +249,7 @@ export function OperationResult({
171
249
  result?: ApiResult<unknown> | null;
172
250
  successLabel?: string;
173
251
  }) {
252
+ const mode = useAppStore((state) => state.mode);
174
253
  if (!result) return null;
175
254
  if (!result.ok) {
176
255
  return <EmptyState title="Request unavailable" detail={result.error || <ValuePreview value={result.data} />} />;
@@ -178,7 +257,7 @@ export function OperationResult({
178
257
  return (
179
258
  <div className="space-y-2 rounded-md border border-border bg-background p-3">
180
259
  <Badge variant="success">{successLabel}</Badge>
181
- <StructuredView value={result.data} />
260
+ {mode === "basic" ? <FriendlySummary value={result.data} /> : <StructuredView value={result.data} />}
182
261
  </div>
183
262
  );
184
263
  }
@@ -194,20 +273,21 @@ export function EntityList({
194
273
  metaKey?: string;
195
274
  limit?: number;
196
275
  }) {
276
+ const mode = useAppStore((state) => state.mode);
197
277
  const rows = asArray<Record<string, unknown>>(items).slice(0, limit);
198
- if (!rows.length) return <EmptyState title="No records" detail="The API returned an empty collection." />;
278
+ if (!rows.length) return <EmptyState title="Nothing here yet" detail="New items will appear here when Lattice has something to show." />;
199
279
  return (
200
280
  <div className="grid gap-2">
201
281
  {rows.map((item, index) => (
202
- <div key={String(item.id || item.name || index)} className="rounded-md border border-border bg-background p-3">
282
+ <div key={String(item.id || item.name || index)} className="rounded-lg border border-border bg-background/55 p-3">
203
283
  <div className="flex flex-wrap items-center justify-between gap-2">
204
- <div className="font-medium">{String(item[titleKey] || item.name || item.id || `Record ${index + 1}`)}</div>
205
- <Badge variant="muted">{String(item[metaKey] || item.status || item.state || "record")}</Badge>
284
+ <div className="font-medium">{mode === "basic" ? humanText(item[titleKey] || item.name || item.label || `Item ${index + 1}`) : String(item[titleKey] || item.name || item.id || `Record ${index + 1}`)}</div>
285
+ <Badge variant="muted">{mode === "basic" ? humanText(item[metaKey] || item.status || item.state || "ready") : String(item[metaKey] || item.status || item.state || "record")}</Badge>
206
286
  </div>
207
287
  {item.summary || item.description || item.path || (item.id && item[titleKey] !== item.id) ? (
208
288
  <p className="mt-1 text-sm text-muted-foreground">{String(item.summary || item.description || item.path || item.id)}</p>
209
289
  ) : null}
210
- {item.id && item[titleKey] !== item.id ? (
290
+ {mode !== "basic" && item.id && item[titleKey] !== item.id ? (
211
291
  <div className="mt-1 text-xs text-muted-foreground">{shortId(item.id, 48)}</div>
212
292
  ) : null}
213
293
  </div>
@@ -216,6 +296,32 @@ export function EntityList({
216
296
  );
217
297
  }
218
298
 
299
+ export function ModeGate({
300
+ title = "Advanced controls",
301
+ detail = "Switch modes when you want diagnostics or administrative controls. Basic mode keeps the product focused on everyday use.",
302
+ target = "advanced",
303
+ }: {
304
+ title?: string;
305
+ detail?: string;
306
+ target?: "advanced" | "admin";
307
+ }) {
308
+ const setMode = useAppStore((state) => state.setMode);
309
+ return (
310
+ <Card>
311
+ <CardContent className="flex flex-col items-start gap-3 p-6">
312
+ <div className="grid h-10 w-10 place-items-center rounded-md border border-border bg-background/70">
313
+ <LockKeyhole className="h-5 w-5 text-primary" />
314
+ </div>
315
+ <div>
316
+ <div className="text-lg font-semibold">{title}</div>
317
+ <p className="mt-1 max-w-2xl text-sm leading-6 text-muted-foreground">{detail}</p>
318
+ </div>
319
+ <Button onClick={() => setMode(target)}>{target === "admin" ? "Switch to Admin" : "Switch to Advanced"}</Button>
320
+ </CardContent>
321
+ </Card>
322
+ );
323
+ }
324
+
219
325
  export function ActionButton({
220
326
  label,
221
327
  successLabel = "Done",
@@ -268,14 +374,14 @@ export function Tabs({
268
374
  onChange: (id: string) => void;
269
375
  }) {
270
376
  return (
271
- <div className="flex flex-wrap gap-1 rounded-md border border-border bg-muted/30 p-1">
377
+ <div className="inline-flex max-w-full flex-wrap gap-1 rounded-lg border border-border bg-muted/28 p-1">
272
378
  {tabs.map((tab) => (
273
379
  <button
274
380
  key={tab.id}
275
381
  onClick={() => onChange(tab.id)}
276
382
  className={cn(
277
- "h-8 rounded px-3 text-sm font-medium transition",
278
- value === tab.id ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
383
+ "h-9 rounded-md px-3.5 text-sm font-semibold transition",
384
+ value === tab.id ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:bg-card/60 hover:text-foreground",
279
385
  )}
280
386
  >
281
387
  {tab.label}
@@ -9,7 +9,7 @@ const variants = {
9
9
  default: "border-primary/25 bg-primary/12 text-primary",
10
10
  success: "border-emerald-500/25 bg-emerald-500/12 text-emerald-300",
11
11
  warning: "border-amber-500/25 bg-amber-500/12 text-amber-300",
12
- muted: "border-border bg-muted text-muted-foreground",
12
+ muted: "border-border bg-muted/70 text-muted-foreground",
13
13
  danger: "border-destructive/30 bg-destructive/12 text-destructive",
14
14
  };
15
15
 
@@ -17,7 +17,7 @@ export function Badge({ className, variant = "default", ...props }: BadgeProps)
17
17
  return (
18
18
  <span
19
19
  className={cn(
20
- "inline-flex min-h-6 items-center rounded-md border px-2 py-0.5 text-xs font-medium",
20
+ "inline-flex min-h-6 max-w-full items-center rounded-md border px-2 py-0.5 text-xs font-semibold leading-none",
21
21
  variants[variant],
22
22
  className,
23
23
  )}
@@ -3,20 +3,20 @@ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "@/lib/utils";
4
4
 
5
5
  const buttonVariants = cva(
6
- "inline-flex h-9 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-45",
6
+ "inline-flex h-10 max-w-full items-center justify-center gap-2 rounded-md px-3.5 text-sm font-semibold leading-none transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-45",
7
7
  {
8
8
  variants: {
9
9
  variant: {
10
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
11
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
12
- ghost: "hover:bg-muted text-foreground",
13
- outline: "border border-border bg-background hover:bg-muted",
10
+ default: "bg-primary text-primary-foreground shadow-[0_10px_30px_hsl(var(--primary)/0.16)] hover:bg-primary/92",
11
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/82",
12
+ ghost: "text-foreground hover:bg-muted",
13
+ outline: "border border-border bg-card/70 hover:bg-muted/88",
14
14
  destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15
15
  },
16
16
  size: {
17
17
  sm: "h-8 px-2.5 text-xs",
18
- md: "h-9 px-3",
19
- icon: "h-9 w-9 px-0",
18
+ md: "h-10 px-3.5",
19
+ icon: "h-10 w-10 px-0",
20
20
  },
21
21
  },
22
22
  defaultVariants: {
@@ -2,21 +2,21 @@ import * as React from "react";
2
2
  import { cn } from "@/lib/utils";
3
3
 
4
4
  export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
5
- return <section className={cn("rounded-lg border border-border bg-card text-card-foreground shadow-sm", className)} {...props} />;
5
+ return <section className={cn("premium-surface rounded-lg text-card-foreground", className)} {...props} />;
6
6
  }
7
7
 
8
8
  export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
9
- return <div className={cn("flex flex-col gap-1.5 p-4", className)} {...props} />;
9
+ return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />;
10
10
  }
11
11
 
12
12
  export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
13
- return <h2 className={cn("text-sm font-semibold tracking-normal", className)} {...props} />;
13
+ return <h2 className={cn("text-base font-semibold tracking-normal", className)} {...props} />;
14
14
  }
15
15
 
16
16
  export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
17
- return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
17
+ return <p className={cn("text-sm leading-6 text-muted-foreground", className)} {...props} />;
18
18
  }
19
19
 
20
20
  export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
21
- return <div className={cn("p-4 pt-0", className)} {...props} />;
21
+ return <div className={cn("p-5 pt-0", className)} {...props} />;
22
22
  }
@@ -6,7 +6,7 @@ export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttribute
6
6
  <input
7
7
  ref={ref}
8
8
  className={cn(
9
- "h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none transition placeholder:text-muted-foreground focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
9
+ "h-10 w-full rounded-md border border-input bg-background/70 px-3 text-sm outline-none transition placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50",
10
10
  className,
11
11
  )}
12
12
  {...props}
@@ -6,7 +6,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTML
6
6
  <textarea
7
7
  ref={ref}
8
8
  className={cn(
9
- "min-h-24 w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm outline-none transition placeholder:text-muted-foreground focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
9
+ "min-h-28 w-full resize-y rounded-md border border-input bg-background/70 px-3 py-3 text-sm leading-6 outline-none transition placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring/35 disabled:cursor-not-allowed disabled:opacity-50",
10
10
  className,
11
11
  )}
12
12
  {...props}