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.
- package/README.md +46 -18
- package/docs/CHANGELOG.md +85 -0
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +49 -0
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +34 -0
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +40 -0
- package/docs/V4_5_0_ONBOARDING_REPORT.md +31 -0
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +49 -0
- package/docs/V4_5_0_VALIDATION_REPORT.md +60 -0
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +33 -0
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +37 -0
- package/docs/V4_5_1_NAVIGATION_REPORT.md +37 -0
- package/docs/V4_5_1_ONBOARDING_REPORT.md +29 -0
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +61 -0
- package/docs/V4_5_1_RC_ARTIFACTS.md +44 -0
- package/docs/V4_5_1_UX_REPORT.md +45 -0
- package/docs/V4_5_1_VALIDATION_REPORT.md +54 -0
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +30 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -16
- package/docs/architecture.md +8 -4
- package/frontend/src/App.tsx +152 -91
- package/frontend/src/api/client.ts +83 -1
- package/frontend/src/components/FirstRunGuide.tsx +99 -0
- package/frontend/src/components/primitives.tsx +131 -25
- package/frontend/src/components/ui/badge.tsx +2 -2
- package/frontend/src/components/ui/button.tsx +7 -7
- package/frontend/src/components/ui/card.tsx +5 -5
- package/frontend/src/components/ui/input.tsx +1 -1
- package/frontend/src/components/ui/textarea.tsx +1 -1
- package/frontend/src/pages/Act.tsx +58 -28
- package/frontend/src/pages/Ask.tsx +51 -19
- package/frontend/src/pages/Brain.tsx +60 -42
- package/frontend/src/pages/Capture.tsx +24 -24
- package/frontend/src/pages/Library.tsx +222 -32
- package/frontend/src/pages/System.tsx +56 -34
- package/frontend/src/routes.ts +15 -13
- package/frontend/src/store/appStore.ts +8 -1
- package/frontend/src/styles.css +666 -36
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/models.py +107 -18
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/model_compat.py +250 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/models/router.py +136 -32
- package/latticeai/services/model_catalog.py +2 -2
- package/latticeai/services/model_recommendation.py +8 -1
- package/latticeai/services/model_runtime.py +18 -3
- package/package.json +1 -1
- package/scripts/build_frontend_assets.mjs +12 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +5 -5
- package/static/app/assets/index-3G8qcrIS.js +336 -0
- package/static/app/assets/index-3G8qcrIS.js.map +1 -0
- package/static/app/assets/index-C0wYZp7k.css +2 -0
- package/static/app/index.html +2 -2
- package/static/app/assets/index-CHHal8Zl.css +0 -2
- package/static/app/assets/index-pdzil9ac.js +0 -333
- 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: "
|
|
18
|
+
{ id: "agents", label: "Goals" },
|
|
18
19
|
{ id: "runs", label: "Runs" },
|
|
19
|
-
{ id: "workflows", label: "
|
|
20
|
-
{ id: "hooks", label: "
|
|
21
|
-
{ id: "tools", label: "
|
|
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-
|
|
31
|
-
<header>
|
|
32
|
-
<div className="
|
|
33
|
-
<h1 className="
|
|
34
|
-
<p className="
|
|
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" />
|
|
71
|
-
<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="
|
|
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
|
|
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="
|
|
88
|
-
{(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
|
|
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="
|
|
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>
|
|
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
|
|
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="
|
|
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
|
-
|
|
291
|
-
<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="
|
|
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="
|
|
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>
|
|
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-
|
|
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-[
|
|
115
|
-
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-
|
|
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
|
-
<
|
|
118
|
-
<p className="text-sm text-muted-foreground">
|
|
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
|
|
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-
|
|
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
|
-
)) :
|
|
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-
|
|
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
|
|
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
|
-
|
|
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="
|
|
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
|
|
194
|
-
<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="
|
|
228
|
+
<CardContent>{trace ? <StructuredView value={trace} /> : <EmptyState title="Ask to see context" />}</CardContent>
|
|
197
229
|
</Card>
|
|
198
230
|
</>
|
|
199
231
|
);
|