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
@@ -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
  );
@@ -1,12 +1,13 @@
1
1
  import * as React from "react";
2
2
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { ImagePlus, MessageSquare, Send, Trash2 } from "lucide-react";
3
+ import { ImagePlus, MessageSquare, Send, Sparkles, Trash2 } from "lucide-react";
4
4
  import { latticeApi } from "@/api/client";
5
5
  import { DataPanel, EmptyState, EntityList, SourceBadge, StructuredView } from "@/components/primitives";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { Button } from "@/components/ui/button";
8
8
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
9
9
  import { Textarea } from "@/components/ui/textarea";
10
+ import { useAppStore } from "@/store/appStore";
10
11
  import { asArray } from "@/lib/utils";
11
12
 
12
13
  type Msg = { role?: string; content?: string; timestamp?: string };
@@ -81,19 +82,25 @@ export function AskPage() {
81
82
  });
82
83
 
83
84
  return (
84
- <div className="grid min-h-[calc(100vh-7rem)] gap-4 xl:grid-cols-[18rem_minmax(0,1fr)_22rem]">
85
+ <div className="space-y-5">
86
+ <header className="page-hero">
87
+ <div className="page-kicker"><MessageSquare className="h-4 w-4" /> Ask</div>
88
+ <h1 className="page-title">Think out loud with Lattice.</h1>
89
+ <p className="page-copy">Ask in plain language, attach an image, and let your private memory shape the answer.</p>
90
+ </header>
91
+ <div className="grid min-h-[calc(100vh-13rem)] gap-4 xl:grid-cols-[18rem_minmax(0,1fr)_22rem]">
85
92
  <Card className="overflow-hidden">
86
93
  <CardHeader>
87
94
  <CardTitle className="flex items-center gap-2"><MessageSquare className="h-4 w-4" /> Conversations</CardTitle>
88
- <CardDescription>Durable history from the backend conversation store.</CardDescription>
95
+ <CardDescription>Pick up where you left off.</CardDescription>
89
96
  </CardHeader>
90
- <CardContent className="space-y-2">
97
+ <CardContent className="soft-scrollbar max-h-[42rem] space-y-2 overflow-auto">
91
98
  <SourceBadge result={history.data} />
92
99
  {asArray<Record<string, unknown>>(history.data?.data).length ? asArray<Record<string, unknown>>(history.data?.data).map((item) => (
93
100
  <button
94
101
  key={String(item.id)}
95
102
  onClick={() => setConversationId(String(item.id))}
96
- className="block w-full rounded-md border border-border bg-background p-3 text-left text-sm transition hover:bg-muted"
103
+ className="block w-full rounded-lg border border-border bg-background/55 p-3 text-left text-sm transition hover:bg-muted"
97
104
  >
98
105
  <div className="font-medium">{String(item.title || item.id)}</div>
99
106
  <div className="mt-1 flex items-center justify-between gap-2 text-xs text-muted-foreground">
@@ -111,28 +118,35 @@ export function AskPage() {
111
118
  </CardContent>
112
119
  </Card>
113
120
 
114
- <section className="flex min-h-[40rem] flex-col rounded-lg border border-border bg-card">
115
- <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-4">
121
+ <section className="premium-surface flex min-h-[42rem] flex-col overflow-hidden rounded-lg">
122
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-5">
116
123
  <div>
117
- <h1 className="text-xl font-semibold">Ask</h1>
118
- <p className="text-sm text-muted-foreground">Streams through `/chat`; no local answer is fabricated when the model is unavailable.</p>
124
+ <h2 className="text-xl font-semibold">New conversation</h2>
125
+ <p className="text-sm text-muted-foreground">Lattice answers only when a real model is available.</p>
119
126
  </div>
120
127
  <div className="flex flex-wrap items-center gap-2">
121
128
  <Badge variant="muted">{String((models.data?.data as Record<string, unknown>)?.current || "no model loaded")}</Badge>
122
129
  <SourceBadge result={models.data} />
123
130
  </div>
124
131
  </div>
125
- <div className="scrollbar-thin flex-1 space-y-3 overflow-auto p-4">
132
+ <div className="soft-scrollbar flex-1 space-y-4 overflow-auto p-5">
126
133
  {messages.length ? messages.map((msg, index) => (
127
134
  <div key={index} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
128
- <div className={`max-w-[78%] rounded-lg border p-3 text-sm ${msg.role === "user" ? "border-primary/30 bg-primary/15" : "border-border bg-background"}`}>
135
+ <div className={`max-w-[78%] rounded-lg border p-4 text-sm leading-6 ${msg.role === "user" ? "border-primary/30 bg-primary/15" : "border-border bg-background/68"}`}>
129
136
  <div className="mb-1 text-xs uppercase text-muted-foreground">{msg.role || "message"}</div>
130
137
  <div className="whitespace-pre-wrap">{msg.content}</div>
131
138
  </div>
132
139
  </div>
133
- )) : <EmptyState title="Ready" detail="Ask a question once the backend and model are available." />}
140
+ )) : (
141
+ <div className="grid min-h-full place-items-center">
142
+ <EmptyState
143
+ title="What should we think through?"
144
+ detail="Ask about a document, a project, a memory, or a question you want Lattice to connect across your workspace."
145
+ />
146
+ </div>
147
+ )}
134
148
  </div>
135
- <div className="border-t border-border p-4">
149
+ <div className="border-t border-border bg-background/28 p-5">
136
150
  {imageData ? <Badge variant="success" className="mb-2">image attached</Badge> : null}
137
151
  <Textarea
138
152
  value={draft}
@@ -143,12 +157,12 @@ export function AskPage() {
143
157
  void send();
144
158
  }
145
159
  }}
146
- placeholder="Ask the brain..."
160
+ placeholder="Ask anything about your work..."
147
161
  />
148
162
  <div className="mt-2 flex flex-wrap items-center justify-between gap-2">
149
163
  <label className="inline-flex h-9 cursor-pointer items-center gap-2 rounded-md border border-border px-3 text-sm hover:bg-muted">
150
164
  <ImagePlus className="h-4 w-4" />
151
- Attach image
165
+ Image
152
166
  <input
153
167
  type="file"
154
168
  accept="image/*"
@@ -169,20 +183,38 @@ export function AskPage() {
169
183
  <aside className="space-y-4">
170
184
  <ContextPreview question={draft || [...messages].reverse().find((m: Msg) => m.role === "user")?.content || ""} trace={trace} />
171
185
  </aside>
186
+ </div>
172
187
  </div>
173
188
  );
174
189
  }
175
190
 
176
191
  function ContextPreview({ question, trace }: { question: string; trace: unknown }) {
192
+ const mode = useAppStore((state) => state.mode);
177
193
  const hybrid = useQuery({
178
194
  queryKey: ["askHybrid", question],
179
195
  queryFn: () => latticeApi.hybridSearch(question),
180
196
  enabled: question.trim().length > 2,
181
197
  });
182
198
  const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
199
+ if (mode === "basic") {
200
+ return (
201
+ <>
202
+ <DataPanel title="Relevant memories" result={hybrid.data}>
203
+ {(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={5} />}
204
+ </DataPanel>
205
+ <Card>
206
+ <CardHeader>
207
+ <CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Sources</CardTitle>
208
+ <CardDescription>Lattice shows supporting memories when an answer uses them.</CardDescription>
209
+ </CardHeader>
210
+ <CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="Ask to see sources" />}</CardContent>
211
+ </Card>
212
+ </>
213
+ );
214
+ }
183
215
  return (
184
216
  <>
185
- <DataPanel title="Retrieval preview" result={hybrid.data}>
217
+ <DataPanel title="Memory preview" result={hybrid.data}>
186
218
  {(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={5} />}
187
219
  </DataPanel>
188
220
  <DataPanel title="Graph context" result={graph.data}>
@@ -190,10 +222,10 @@ function ContextPreview({ question, trace }: { question: string; trace: unknown
190
222
  </DataPanel>
191
223
  <Card>
192
224
  <CardHeader>
193
- <CardTitle>Why this context</CardTitle>
194
- <CardDescription>Trace emitted by `/chat` when the backend includes it.</CardDescription>
225
+ <CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Why this context</CardTitle>
226
+ <CardDescription>Signals Lattice used to choose supporting memories.</CardDescription>
195
227
  </CardHeader>
196
- <CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="No trace yet" />}</CardContent>
228
+ <CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="Ask to see context" />}</CardContent>
197
229
  </Card>
198
230
  </>
199
231
  );