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
@@ -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}
@@ -3,37 +3,39 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
3
  import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
4
  import { Bot, GitBranch, PauseCircle, Play, Workflow } from "lucide-react";
5
5
  import { latticeApi } from "@/api/client";
6
- import { ActionButton, DataPanel, EntityList, KeyValueList, OperationResult, StructuredView, Tabs } from "@/components/primitives";
6
+ import { ActionButton, DataPanel, EntityList, KeyValueList, ModeGate, OperationResult, StructuredView, Tabs } from "@/components/primitives";
7
7
  import { Badge } from "@/components/ui/badge";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
10
  import { Input } from "@/components/ui/input";
11
11
  import { Textarea } from "@/components/ui/textarea";
12
+ import { useAppStore } from "@/store/appStore";
12
13
  import { asArray, shortId } from "@/lib/utils";
13
14
 
14
15
  type ActTab = "agents" | "runs" | "workflows" | "hooks" | "tools";
15
16
 
16
17
  const tabs: Array<{ id: ActTab; label: string }> = [
17
- { id: "agents", label: "Agents" },
18
+ { id: "agents", label: "Goals" },
18
19
  { id: "runs", label: "Runs" },
19
- { id: "workflows", label: "Workflows" },
20
- { id: "hooks", label: "Hooks" },
21
- { id: "tools", label: "Tools" },
20
+ { id: "workflows", label: "Recipes" },
21
+ { id: "hooks", label: "Safeguards" },
22
+ { id: "tools", label: "Permissions" },
22
23
  ];
23
24
 
24
25
  export function ActPage({ initialTab }: { initialTab?: string }) {
26
+ const mode = useAppStore((state) => state.mode);
25
27
  const [tab, setTab] = React.useState<ActTab>((initialTab as ActTab) || "agents");
26
28
  React.useEffect(() => {
27
29
  if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as ActTab);
28
30
  }, [initialTab]);
29
31
  return (
30
- <div className="space-y-4">
31
- <header>
32
- <div className="flex items-center gap-2 text-sm text-primary"><Workflow className="h-4 w-4" /> Durable execution</div>
33
- <h1 className="mt-2 text-3xl font-semibold">Act</h1>
34
- <p className="mt-2 max-w-3xl text-sm text-muted-foreground">Agents, workflows, approvals, hooks, and governed tools. Pauses and unavailable states are surfaced honestly.</p>
32
+ <div className="space-y-5">
33
+ <header className="page-hero">
34
+ <div className="page-kicker"><Workflow className="h-4 w-4" /> Automate</div>
35
+ <h1 className="page-title">Make work move, with a hand on the door.</h1>
36
+ <p className="page-copy">Give Lattice a goal, review each run, and approve sensitive actions before anything important changes.</p>
35
37
  </header>
36
- <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as ActTab)} />
38
+ <Tabs tabs={tabs.map((item) => mode === "basic" ? item : item.id === "hooks" ? { ...item, label: "Hooks" } : item.id === "tools" ? { ...item, label: "Tools" } : item)} value={tab} onChange={(id) => setTab(id as ActTab)} />
37
39
  {tab === "agents" ? <AgentsPanel /> : null}
38
40
  {tab === "runs" ? <RunsPanel /> : null}
39
41
  {tab === "workflows" ? <WorkflowsPanel /> : null}
@@ -45,6 +47,7 @@ export function ActPage({ initialTab }: { initialTab?: string }) {
45
47
 
46
48
  function AgentsPanel() {
47
49
  const qc = useQueryClient();
50
+ const mode = useAppStore((state) => state.mode);
48
51
  const [goal, setGoal] = React.useState("");
49
52
  const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
50
53
  const registry = useQuery({ queryKey: ["agentRegistry"], queryFn: latticeApi.agentRegistry });
@@ -61,17 +64,17 @@ function AgentsPanel() {
61
64
  const runtimeData = (runtime.data?.data || {}) as Record<string, unknown>;
62
65
  const runtimeMeta = (runtimeData.runtime || {}) as Record<string, unknown>;
63
66
  const runtimeReady = Boolean(runtimeMeta.ready);
64
- const runtimeReason = String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
67
+ const runtimeReason = mode === "basic" ? "Load a local model before running agents." : String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
65
68
  const canRunAgent = Boolean(goal.trim()) && runtimeReady && !run.isPending;
66
69
  return (
67
70
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
68
71
  <Card>
69
72
  <CardHeader>
70
- <CardTitle className="flex items-center gap-2"><Bot className="h-4 w-4" /> Run agent pipeline</CardTitle>
71
- <CardDescription>POST `/agents/api/run` creates a durable run; mode is determined by backend model availability.</CardDescription>
73
+ <CardTitle className="flex items-center gap-2"><Bot className="h-4 w-4" /> Start with a goal</CardTitle>
74
+ <CardDescription>Lattice will plan, execute, and review only when the local model is ready.</CardDescription>
72
75
  </CardHeader>
73
76
  <CardContent className="space-y-3">
74
- <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="Describe the objective..." />
77
+ <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="What should Lattice help you accomplish?" />
75
78
  {!runtimeReady ? <Badge variant="warning">{runtimeReason}</Badge> : null}
76
79
  <Button
77
80
  className="w-full"
@@ -79,15 +82,30 @@ function AgentsPanel() {
79
82
  disabled={!canRunAgent}
80
83
  onClick={() => run.mutate()}
81
84
  >
82
- <Play className="h-4 w-4" /> {runtimeReady ? "Run pipeline" : "Agent execution unavailable"}
85
+ <Play className="h-4 w-4" /> {runtimeReady ? "Start Run" : "Load a model first"}
83
86
  </Button>
84
87
  {run.data ? <OperationResult result={run.data} successLabel="Agent run request completed" /> : null}
85
88
  </CardContent>
86
89
  </Card>
87
- <DataPanel title="Runtime status" result={runtime.data}>
88
- {(data) => <StructuredView value={data} />}
90
+ <DataPanel title="Readiness" result={runtime.data}>
91
+ {(data) => mode === "basic" ? (
92
+ <div className="grid gap-3 sm:grid-cols-3">
93
+ <div className="rounded-lg border border-border bg-background/55 p-3">
94
+ <div className="text-sm font-medium">Model</div>
95
+ <Badge variant={runtimeReady ? "success" : "warning"}>{runtimeReady ? "ready" : "needed"}</Badge>
96
+ </div>
97
+ <div className="rounded-lg border border-border bg-background/55 p-3">
98
+ <div className="text-sm font-medium">Planner</div>
99
+ <Badge variant="muted">{runtimeReady ? "available" : "waiting"}</Badge>
100
+ </div>
101
+ <div className="rounded-lg border border-border bg-background/55 p-3">
102
+ <div className="text-sm font-medium">Review</div>
103
+ <Badge variant="success">approval required</Badge>
104
+ </div>
105
+ </div>
106
+ ) : <StructuredView value={data} />}
89
107
  </DataPanel>
90
- <DataPanel title="Agent registry" result={registry.data}>
108
+ <DataPanel title="Agent team" result={registry.data}>
91
109
  {(data) => (
92
110
  <div className="space-y-3">
93
111
  <EntityList items={(data as Record<string, unknown>).agents} titleKey="name" metaKey="type" />
@@ -98,7 +116,7 @@ function AgentsPanel() {
98
116
  </div>
99
117
  )}
100
118
  </DataPanel>
101
- <DataPanel title="Agent capabilities" result={caps.data}>
119
+ <DataPanel title={mode === "basic" ? "What Lattice can do" : "What agents can do"} result={caps.data}>
102
120
  {(data) => <StructuredView value={data} />}
103
121
  </DataPanel>
104
122
  </div>
@@ -106,6 +124,7 @@ function AgentsPanel() {
106
124
  }
107
125
 
108
126
  function RunsPanel() {
127
+ const mode = useAppStore((state) => state.mode);
109
128
  const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
110
129
  const workflows = useQuery({ queryKey: ["workflowRuns"], queryFn: latticeApi.workflowRuns });
111
130
  const pending = useQuery({ queryKey: ["permissions"], queryFn: latticeApi.permissionsPending });
@@ -125,10 +144,10 @@ function RunsPanel() {
125
144
  const rows = Object.entries(pendingMap);
126
145
  return rows.length ? (
127
146
  <div className="grid gap-2">
128
- {rows.map(([token, value]) => (
147
+ {rows.map(([token, value], index) => (
129
148
  <div key={token} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border p-3">
130
149
  <div>
131
- <div className="font-medium">{shortId(token, 16)}</div>
150
+ <div className="font-medium">{mode === "basic" ? `Approval request ${index + 1}` : shortId(token, 16)}</div>
132
151
  <div className="mt-2">
133
152
  <KeyValueList data={(value || {}) as Record<string, unknown>} limit={5} />
134
153
  </div>
@@ -209,7 +228,7 @@ function WorkflowsPanel() {
209
228
  <Card>
210
229
  <CardHeader>
211
230
  <CardTitle className="flex items-center gap-2"><GitBranch className="h-4 w-4" /> Workflow graph</CardTitle>
212
- <CardDescription>React Flow view of workflow definitions. Running a workflow calls its backend run endpoint.</CardDescription>
231
+ <CardDescription>See your saved workflows as a simple map.</CardDescription>
213
232
  </CardHeader>
214
233
  <CardContent>
215
234
  <div className="h-[440px] rounded-lg border border-border">
@@ -228,7 +247,7 @@ function WorkflowsPanel() {
228
247
  <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Workflow name" />
229
248
  <Button disabled={create.isPending} onClick={() => create.mutate()}>Create</Button>
230
249
  </div>
231
- <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste exported workflow JSON" />
250
+ <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste a workflow export" />
232
251
  <Button variant="outline" disabled={!importText.trim() || importWorkflow.isPending} onClick={() => importWorkflow.mutate()}>Import</Button>
233
252
  {create.data ? <OperationResult result={create.data} successLabel="Workflow created" /> : null}
234
253
  {importWorkflow.data ? <OperationResult result={importWorkflow.data} successLabel="Workflow imported" /> : null}
@@ -248,7 +267,7 @@ function WorkflowsPanel() {
248
267
  </div>
249
268
  )}
250
269
  </DataPanel>
251
- <DataPanel title="Trigger configuration" result={triggers.data} className="xl:col-span-2">
270
+ <DataPanel title="Automation triggers" result={triggers.data} className="xl:col-span-2">
252
271
  {(data) => <StructuredView value={data} />}
253
272
  </DataPanel>
254
273
  </div>
@@ -275,8 +294,19 @@ function manualWorkflowNodes(): Array<Record<string, unknown>> {
275
294
  }
276
295
 
277
296
  function HooksPanel() {
297
+ const mode = useAppStore((state) => state.mode);
278
298
  const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
279
299
  const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
300
+ if (mode === "basic") {
301
+ return (
302
+ <div className="grid gap-4 xl:grid-cols-2">
303
+ <DataPanel title="Safeguards" result={hooks.data}>
304
+ {(data) => <EntityList items={(data as Record<string, unknown>).hooks} titleKey="name" metaKey="kind" />}
305
+ </DataPanel>
306
+ <ModeGate title="Detailed hook logs" detail="Switch to Advanced when you need hook run logs and manual diagnostic controls." />
307
+ </div>
308
+ );
309
+ }
280
310
  return (
281
311
  <div className="grid gap-4 xl:grid-cols-2">
282
312
  <DataPanel title="Hooks" result={hooks.data}>
@@ -287,8 +317,8 @@ function HooksPanel() {
287
317
  </DataPanel>
288
318
  <Card className="xl:col-span-2">
289
319
  <CardHeader>
290
- <CardTitle className="flex items-center gap-2"><PauseCircle className="h-4 w-4" /> Manual hook fire</CardTitle>
291
- <CardDescription>Uses `/api/hooks/run`; no hook is treated as successful unless the backend records it.</CardDescription>
320
+ <CardTitle className="flex items-center gap-2"><PauseCircle className="h-4 w-4" /> Run manual hooks</CardTitle>
321
+ <CardDescription>Trigger hooks deliberately and review the recorded result.</CardDescription>
292
322
  </CardHeader>
293
323
  <CardContent>
294
324
  <ActionButton label="Run all manual hooks" action={() => latticeApi.hookRun({ event: "manual" })} invalidate={["hookRuns"]} />
@@ -301,7 +331,7 @@ function HooksPanel() {
301
331
  function ToolsPanel() {
302
332
  const tools = useQuery({ queryKey: ["toolPermissions"], queryFn: latticeApi.toolPermissions });
303
333
  return (
304
- <DataPanel title="Tool governance" result={tools.data}>
334
+ <DataPanel title="Action permissions" result={tools.data}>
305
335
  {(data) => <EntityList items={(data as Record<string, unknown>).permissions || data} titleKey="tool" metaKey="risk" />}
306
336
  </DataPanel>
307
337
  );