ltcai 7.9.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +35 -49
  2. package/docs/CHANGELOG.md +35 -0
  3. package/docs/V3_FRONTEND.md +11 -5
  4. package/lattice_brain/__init__.py +1 -1
  5. package/lattice_brain/embeddings.py +9 -1
  6. package/lattice_brain/graph/projection.py +19 -5
  7. package/lattice_brain/graph/provenance.py +26 -4
  8. package/lattice_brain/graph/write_master.py +30 -3
  9. package/lattice_brain/runtime/multi_agent.py +1 -1
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/app_factory.py +13 -134
  12. package/latticeai/core/local_embeddings.py +8 -0
  13. package/latticeai/core/marketplace.py +1 -1
  14. package/latticeai/core/mcp_registry.py +140 -1
  15. package/latticeai/core/tool_registry.py +7 -0
  16. package/latticeai/core/workspace_os.py +1 -1
  17. package/latticeai/services/architecture_readiness.py +50 -6
  18. package/latticeai/services/model_runtime.py +36 -2
  19. package/latticeai/services/product_readiness.py +19 -17
  20. package/package.json +2 -2
  21. package/scripts/build_frontend_assets.mjs +11 -2
  22. package/src-tauri/Cargo.lock +1 -1
  23. package/src-tauri/Cargo.toml +1 -1
  24. package/src-tauri/tauri.conf.json +1 -1
  25. package/static/app/asset-manifest.json +11 -102
  26. package/static/app/assets/Act-BOO66G-c.js +1 -0
  27. package/static/app/assets/Brain-C6lEYiD7.js +321 -0
  28. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
  29. package/static/app/assets/Library-DK4FIp8a.js +1 -0
  30. package/static/app/assets/System-Bgs6Ql7x.js +1 -0
  31. package/static/app/assets/core-CwxXejkd.js +1 -2
  32. package/static/app/assets/index-Bvv79nre.js +16 -0
  33. package/static/app/assets/index-Dslqglia.css +2 -0
  34. package/static/app/assets/primitives-B70WOra0.js +1 -0
  35. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
  36. package/static/app/index.html +3 -10
  37. package/static/app/theme-boot.js +8 -0
  38. package/static/sw.js +1 -1
  39. package/frontend/index.html +0 -24
  40. package/frontend/openapi.json +0 -15425
  41. package/frontend/src/App.tsx +0 -243
  42. package/frontend/src/api/client.ts +0 -580
  43. package/frontend/src/api/openapi.ts +0 -17892
  44. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  45. package/frontend/src/components/FeedbackState.tsx +0 -45
  46. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  47. package/frontend/src/components/LivingBrain.tsx +0 -220
  48. package/frontend/src/components/ProductFlow.tsx +0 -171
  49. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  50. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  51. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  52. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  53. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  54. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  55. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  56. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  57. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  58. package/frontend/src/components/primitives.tsx +0 -392
  59. package/frontend/src/components/ui/badge.tsx +0 -27
  60. package/frontend/src/components/ui/button.tsx +0 -37
  61. package/frontend/src/components/ui/card.tsx +0 -22
  62. package/frontend/src/components/ui/input.tsx +0 -16
  63. package/frontend/src/components/ui/textarea.tsx +0 -16
  64. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  65. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  66. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  67. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  68. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  69. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  70. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  71. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  72. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  73. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  74. package/frontend/src/features/brain/brainData.ts +0 -246
  75. package/frontend/src/features/brain/graphLayout.ts +0 -37
  76. package/frontend/src/features/brain/types.ts +0 -150
  77. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  78. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  79. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  80. package/frontend/src/i18n.ts +0 -1303
  81. package/frontend/src/lib/utils.ts +0 -33
  82. package/frontend/src/main.tsx +0 -23
  83. package/frontend/src/pages/Act.tsx +0 -458
  84. package/frontend/src/pages/Ask.tsx +0 -14
  85. package/frontend/src/pages/Brain.tsx +0 -914
  86. package/frontend/src/pages/Capture.tsx +0 -258
  87. package/frontend/src/pages/Library.tsx +0 -486
  88. package/frontend/src/pages/System.tsx +0 -621
  89. package/frontend/src/routes.ts +0 -92
  90. package/frontend/src/store/appStore.ts +0 -94
  91. package/frontend/src/styles.css +0 -6579
  92. package/static/app/assets/Act-DOvf59ru.js +0 -2
  93. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  94. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  95. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  96. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  97. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  98. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  99. package/static/app/assets/System-D6t9jo9V.js +0 -2
  100. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  101. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  102. package/static/app/assets/index-C7g26IF6.css +0 -2
  103. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  104. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  105. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  106. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  107. package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
@@ -1,33 +0,0 @@
1
- import { clsx, type ClassValue } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs));
6
- }
7
-
8
- export function fmtNumber(value: unknown, fallback = "0") {
9
- const n = Number(value);
10
- if (!Number.isFinite(n)) return fallback;
11
- return new Intl.NumberFormat().format(n);
12
- }
13
-
14
- export function pct(value: unknown) {
15
- const n = Number(value);
16
- if (!Number.isFinite(n)) return "0%";
17
- return `${Math.round(n * 100)}%`;
18
- }
19
-
20
- export function shortId(value: unknown, length = 10) {
21
- const text = String(value || "");
22
- return text.length > length ? `${text.slice(0, length)}...` : text;
23
- }
24
-
25
- export function asArray<T = Record<string, unknown>>(value: unknown): T[] {
26
- return Array.isArray(value) ? (value as T[]) : [];
27
- }
28
-
29
- export function titleize(value: unknown) {
30
- return String(value || "")
31
- .replace(/[_-]+/g, " ")
32
- .replace(/\b\w/g, (m) => m.toUpperCase());
33
- }
@@ -1,23 +0,0 @@
1
- import React from "react";
2
- import ReactDOM from "react-dom/client";
3
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
- import App from "./App";
5
- import "./styles.css";
6
-
7
- const queryClient = new QueryClient({
8
- defaultOptions: {
9
- queries: {
10
- staleTime: 15_000,
11
- refetchOnWindowFocus: false,
12
- retry: 1,
13
- },
14
- },
15
- });
16
-
17
- ReactDOM.createRoot(document.getElementById("root")!).render(
18
- <React.StrictMode>
19
- <QueryClientProvider client={queryClient}>
20
- <App />
21
- </QueryClientProvider>
22
- </React.StrictMode>,
23
- );
@@ -1,458 +0,0 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import ReactFlow, { Background, Controls, Edge, Node } from "reactflow";
4
- import { Bot, CalendarClock, GitBranch, PauseCircle, Play, ShieldCheck, Workflow } from "lucide-react";
5
- import { latticeApi } from "@/api/client";
6
- import { ActionButton, DataPanel, EntityList, KeyValueList, ModeGate, OperationResult, StructuredView, Tabs } from "@/components/primitives";
7
- import { Badge } from "@/components/ui/badge";
8
- import { Button } from "@/components/ui/button";
9
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
10
- import { Input } from "@/components/ui/input";
11
- import { Textarea } from "@/components/ui/textarea";
12
- import { ReviewInbox } from "@/features/review/ReviewInbox";
13
- import { useAppStore } from "@/store/appStore";
14
- import { asArray, shortId } from "@/lib/utils";
15
- import { t } from "@/i18n";
16
-
17
- type ActTab = "agents" | "runs" | "workflows" | "hooks" | "tools";
18
- type RunsSubTab = "runs" | "review";
19
-
20
- const runsSubTabs: Array<{ id: RunsSubTab; labelKey: string }> = [
21
- { id: "runs", labelKey: "act.tab.runs" },
22
- { id: "review", labelKey: "act.tab.review" },
23
- ];
24
-
25
- const tabs: Array<{ id: ActTab; labelKey: string; advancedLabelKey?: string }> = [
26
- { id: "agents", labelKey: "act.tab.goals" },
27
- { id: "runs", labelKey: "act.tab.runs" },
28
- { id: "workflows", labelKey: "act.tab.recipes" },
29
- { id: "hooks", labelKey: "act.tab.safeguards", advancedLabelKey: "act.tab.hooks" },
30
- { id: "tools", labelKey: "act.tab.permissions", advancedLabelKey: "act.tab.tools" },
31
- ];
32
-
33
- export function ActPage({ initialTab }: { initialTab?: string }) {
34
- const mode = useAppStore((state) => state.mode);
35
- const language = useAppStore((state) => state.language);
36
- const [tab, setTab] = React.useState<ActTab>(() => {
37
- if (initialTab === "review") return "runs";
38
- return (initialTab as ActTab) || "agents";
39
- });
40
- const [runsSubTab, setRunsSubTab] = React.useState<RunsSubTab>(initialTab === "review" ? "review" : "runs");
41
- React.useEffect(() => {
42
- if (initialTab === "review") {
43
- setTab("runs");
44
- setRunsSubTab("review");
45
- return;
46
- }
47
- if (tabs.some((item) => item.id === initialTab)) setTab(initialTab as ActTab);
48
- }, [initialTab]);
49
- return (
50
- <div className="space-y-5">
51
- <header className="page-hero">
52
- <div className="page-kicker"><Workflow className="h-4 w-4" /> {t(language, "act.kicker")}</div>
53
- <h1 className="page-title">{t(language, "act.title")}</h1>
54
- <p className="page-copy">{t(language, "act.copy")}</p>
55
- </header>
56
- <Tabs
57
- tabs={tabs.map((item) => ({
58
- id: item.id,
59
- label: t(language, mode === "basic" || !item.advancedLabelKey ? item.labelKey : item.advancedLabelKey),
60
- }))}
61
- value={tab}
62
- onChange={(id) => setTab(id as ActTab)}
63
- />
64
- {tab === "agents" ? <AgentsPanel /> : null}
65
- {tab === "runs" ? <RunsPanel subTab={runsSubTab} onSubTabChange={setRunsSubTab} /> : null}
66
- {tab === "workflows" ? <WorkflowsPanel /> : null}
67
- {tab === "hooks" ? <HooksPanel /> : null}
68
- {tab === "tools" ? <ToolsPanel /> : null}
69
- </div>
70
- );
71
- }
72
-
73
- function AgentsPanel() {
74
- const qc = useQueryClient();
75
- const mode = useAppStore((state) => state.mode);
76
- const [goal, setGoal] = React.useState("");
77
- const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
78
- const registry = useQuery({ queryKey: ["agentRegistry"], queryFn: latticeApi.agentRegistry });
79
- const caps = useQuery({ queryKey: ["agentCapabilities"], queryFn: latticeApi.agentCapabilities });
80
- const run = useMutation({
81
- mutationFn: () => latticeApi.runAgent(goal, ["planner", "executor", "reviewer"]),
82
- onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRuntime"] }),
83
- });
84
- const [agentName, setAgentName] = React.useState("");
85
- const register = useMutation({
86
- mutationFn: () => latticeApi.registerAgent({ name: agentName, type: "custom", capabilities: [] }),
87
- onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRegistry"] }),
88
- });
89
- const runtimeData = (runtime.data?.data || {}) as Record<string, unknown>;
90
- const runtimeMeta = (runtimeData.runtime || {}) as Record<string, unknown>;
91
- const runtimeReady = Boolean(runtimeMeta.ready);
92
- const runtimeReason = mode === "basic" ? "Load a local model before running agents." : String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
93
- const canRunAgent = Boolean(goal.trim()) && runtimeReady && !run.isPending;
94
- return (
95
- <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
96
- <Card>
97
- <CardHeader>
98
- <CardTitle className="flex items-center gap-2"><Bot className="h-4 w-4" /> Start with a goal</CardTitle>
99
- <CardDescription>Lattice will plan, execute, and review only when the local model is ready.</CardDescription>
100
- </CardHeader>
101
- <CardContent className="space-y-3">
102
- <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="What should Lattice help you accomplish?" />
103
- {!runtimeReady ? <Badge variant="warning">{runtimeReason}</Badge> : null}
104
- <Button
105
- className="w-full"
106
- variant={runtimeReady ? "default" : "outline"}
107
- disabled={!canRunAgent}
108
- onClick={() => run.mutate()}
109
- >
110
- <Play className="h-4 w-4" /> {runtimeReady ? "Start Run" : "Load a model first"}
111
- </Button>
112
- {run.data ? <OperationResult result={run.data} successLabel="Agent run request completed" /> : null}
113
- </CardContent>
114
- </Card>
115
- <DataPanel title="Readiness" result={runtime.data}>
116
- {(data) => mode === "basic" ? (
117
- <div className="grid gap-3 sm:grid-cols-3">
118
- <div className="rounded-lg border border-border bg-background/55 p-3">
119
- <div className="text-sm font-medium">Model</div>
120
- <Badge variant={runtimeReady ? "success" : "warning"}>{runtimeReady ? "ready" : "needed"}</Badge>
121
- </div>
122
- <div className="rounded-lg border border-border bg-background/55 p-3">
123
- <div className="text-sm font-medium">Planner</div>
124
- <Badge variant="muted">{runtimeReady ? "available" : "waiting"}</Badge>
125
- </div>
126
- <div className="rounded-lg border border-border bg-background/55 p-3">
127
- <div className="text-sm font-medium">Review</div>
128
- <Badge variant="success">approval required</Badge>
129
- </div>
130
- </div>
131
- ) : <StructuredView value={data} />}
132
- </DataPanel>
133
- <DataPanel title="Agent team" result={registry.data}>
134
- {(data) => (
135
- <div className="space-y-3">
136
- <EntityList items={(data as Record<string, unknown>).agents} titleKey="name" metaKey="type" />
137
- <div className="flex gap-2">
138
- <Input value={agentName} onChange={(e) => setAgentName(e.target.value)} placeholder="New custom agent name" />
139
- <Button disabled={!agentName.trim() || register.isPending} onClick={() => register.mutate()}>Register</Button>
140
- </div>
141
- </div>
142
- )}
143
- </DataPanel>
144
- <DataPanel title={mode === "basic" ? "What Lattice can do" : "What agents can do"} result={caps.data}>
145
- {(data) => <StructuredView value={data} />}
146
- </DataPanel>
147
- </div>
148
- );
149
- }
150
-
151
- function RunsPanel({ subTab, onSubTabChange }: { subTab: RunsSubTab; onSubTabChange: (tab: RunsSubTab) => void }) {
152
- const language = useAppStore((state) => state.language);
153
- return (
154
- <div className="space-y-4">
155
- <Tabs
156
- tabs={runsSubTabs.map((item) => ({ id: item.id, label: t(language, item.labelKey) }))}
157
- value={subTab}
158
- onChange={(id) => onSubTabChange(id as RunsSubTab)}
159
- />
160
- {subTab === "runs" ? <RunsListPanel /> : <ReviewInbox />}
161
- </div>
162
- );
163
- }
164
-
165
- function RunsListPanel() {
166
- const mode = useAppStore((state) => state.mode);
167
- const runtime = useQuery({ queryKey: ["agentRuntime"], queryFn: latticeApi.agentRuntime });
168
- const workflows = useQuery({ queryKey: ["workflowRuns"], queryFn: latticeApi.workflowRuns });
169
- const pending = useQuery({ queryKey: ["permissions"], queryFn: latticeApi.permissionsPending });
170
- const agentRuns = asArray<Record<string, unknown>>((runtime.data?.data as Record<string, unknown>)?.runs);
171
- const workflowRuns = asArray<Record<string, unknown>>((workflows.data?.data as Record<string, unknown>)?.runs);
172
- return (
173
- <div className="grid gap-4 xl:grid-cols-2">
174
- <DataPanel title="Agent runs" result={runtime.data}>
175
- {() => <RunList runs={agentRuns} kind="agent" />}
176
- </DataPanel>
177
- <DataPanel title="Workflow runs" result={workflows.data}>
178
- {() => <RunList runs={workflowRuns} kind="workflow" />}
179
- </DataPanel>
180
- <DataPanel title="Approval inbox" result={pending.data} className="xl:col-span-2">
181
- {(data) => {
182
- const pendingMap = ((data as Record<string, unknown>).pending || {}) as Record<string, unknown>;
183
- const rows = Object.entries(pendingMap);
184
- return rows.length ? (
185
- <div className="grid gap-2">
186
- {rows.map(([token, value], index) => (
187
- <div key={token} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border p-3">
188
- <div>
189
- <div className="font-medium">{mode === "basic" ? `Approval request ${index + 1}` : shortId(token, 16)}</div>
190
- <div className="mt-2">
191
- <KeyValueList data={(value || {}) as Record<string, unknown>} limit={5} />
192
- </div>
193
- </div>
194
- <div className="flex gap-2">
195
- <ActionButton label="Approve" action={() => latticeApi.approvePermission(token)} invalidate={["permissions"]} />
196
- <ActionButton label="Deny" action={() => latticeApi.denyPermission(token)} invalidate={["permissions"]} variant="destructive" />
197
- </div>
198
- </div>
199
- ))}
200
- </div>
201
- ) : <EntityList items={[]} />;
202
- }}
203
- </DataPanel>
204
- </div>
205
- );
206
- }
207
-
208
- function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "agent" | "workflow" }) {
209
- if (!runs.length) return <EntityList items={[]} />;
210
- return (
211
- <div className="grid gap-2">
212
- {runs.slice(0, 10).map((run) => {
213
- const id = String(run.run_id || run.id);
214
- const status = String(run.status || "unknown");
215
- return (
216
- <div key={id} className="rounded-md border border-border bg-background p-3">
217
- <div className="flex flex-wrap items-center justify-between gap-2">
218
- <div className="font-medium">{shortId(id, 18)}</div>
219
- <Badge variant={status === "succeeded" ? "success" : status === "awaiting_approval" ? "warning" : "muted"}>{status}</Badge>
220
- </div>
221
- <div className="mt-2 flex flex-wrap gap-2">
222
- <ActionButton label="Stop" action={() => kind === "agent" ? latticeApi.stopAgentRun(id) : latticeApi.stopWorkflowRun(id)} />
223
- {status === "awaiting_approval" && kind === "workflow" ? (
224
- <>
225
- <ActionButton label="Resume approved" action={() => latticeApi.resumeWorkflowRun(id, true)} />
226
- <ActionButton label="Resume denied" action={() => latticeApi.resumeWorkflowRun(id, false)} variant="destructive" />
227
- </>
228
- ) : null}
229
- </div>
230
- </div>
231
- );
232
- })}
233
- </div>
234
- );
235
- }
236
-
237
- function WorkflowsPanel() {
238
- const qc = useQueryClient();
239
- const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
240
- const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
241
- const recipes = useQuery({ queryKey: ["automationRecipes"], queryFn: latticeApi.automationRecipes });
242
- const [name, setName] = React.useState("Manual workflow");
243
- const [importText, setImportText] = React.useState("");
244
- const create = useMutation({
245
- mutationFn: () => latticeApi.createWorkflow({
246
- name: name.trim() || "Manual workflow",
247
- nodes: manualWorkflowNodes(),
248
- metadata: { created_from: "desktop-act-ui" },
249
- }),
250
- onSuccess: () => qc.invalidateQueries({ queryKey: ["workflowDefinitions"] }),
251
- });
252
- const importWorkflow = useMutation({
253
- mutationFn: () => latticeApi.importWorkflow(JSON.parse(importText) as Record<string, unknown>),
254
- onSuccess: () => {
255
- setImportText("");
256
- qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
257
- },
258
- });
259
- const installRecipe = useMutation({
260
- mutationFn: (recipeId: string) => latticeApi.installAutomationRecipe(recipeId, false),
261
- onSuccess: () => {
262
- qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
263
- qc.invalidateQueries({ queryKey: ["workflowTriggers"] });
264
- },
265
- });
266
- const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
267
- const installedRecipeIds = new Set(
268
- workflows
269
- .filter((w: any) => w && w.metadata && w.metadata.created_from === "brain_automation_recipe" && w.metadata.recipe_id)
270
- .map((w: any) => String(w.metadata.recipe_id))
271
- );
272
- const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
273
- id: String(workflow.id || workflow.workflow_id || index),
274
- position: { x: (index % 4) * 190, y: Math.floor(index / 4) * 120 },
275
- data: { label: String(workflow.name || workflow.id || `Workflow ${index + 1}`) },
276
- }));
277
- const edges: Edge[] = nodes.slice(1).map((node, index) => ({ id: `e-${index}`, source: nodes[index].id, target: node.id }));
278
- return (
279
- <div className="grid gap-4 xl:grid-cols-[1.2fr_0.8fr]">
280
- <DataPanel title="Brain automations" result={recipes.data} className="xl:col-span-2">
281
- {(data) => {
282
- const items = asArray<Record<string, unknown>>((data as Record<string, unknown>).recipes);
283
- return (
284
- <div className="grid gap-3 lg:grid-cols-3">
285
- {items.map((recipe) => {
286
- const id = String(recipe.id || "");
287
- const consent = (recipe.consent || {}) as Record<string, unknown>;
288
- const creates = asArray<string>(recipe.creates);
289
- return (
290
- <div key={id} className="rounded-lg border border-border bg-background/70 p-4">
291
- <div className="flex items-start justify-between gap-2">
292
- <div>
293
- <div className="flex items-center gap-2 font-medium">
294
- <CalendarClock className="h-4 w-4" /> {String(recipe.name || id)}
295
- </div>
296
- <p className="mt-2 text-sm text-muted-foreground">{String(recipe.summary || "")}</p>
297
- </div>
298
- <Badge variant="muted">{String(recipe.cadence || "draft")}</Badge>
299
- </div>
300
- <p className="mt-3 text-sm">{String(recipe.user_value || "")}</p>
301
- <div className="mt-3 flex flex-wrap gap-2">
302
- <Badge variant="success"><ShieldCheck className="h-3 w-3" /> local only</Badge>
303
- {consent.requires_user_enable ? <Badge variant="warning">draft first</Badge> : null}
304
- {creates.slice(0, 2).map((item) => <Badge key={item} variant="muted">{item}</Badge>)}
305
- </div>
306
- {(() => {
307
- const isInstalling = installRecipe.isPending && installRecipe.variables === id;
308
- const last = installRecipe.data as any;
309
- const lastRid = last && last.recipe && last.recipe.recipe_id ? String(last.recipe.recipe_id) : "";
310
- const justSucceeded = !installRecipe.isPending && lastRid === id;
311
- const installed = installedRecipeIds.has(id);
312
- const btnLabel = isInstalling
313
- ? "Creating..."
314
- : justSucceeded
315
- ? "✓ Draft created"
316
- : installed
317
- ? "Draft already created"
318
- : "Create reviewable draft";
319
- return (
320
- <>
321
- <Button
322
- className="mt-4 w-full"
323
- variant={installed || justSucceeded ? "secondary" : "outline"}
324
- disabled={!id || isInstalling || installed}
325
- onClick={() => {
326
- if (installed || isInstalling) return;
327
- installRecipe.mutate(id);
328
- }}
329
- >
330
- {btnLabel}
331
- </Button>
332
- {justSucceeded ? (
333
- <p className="mt-1 text-[10px] text-green-600">Reviewable draft ready. Check Definitions below.</p>
334
- ) : null}
335
- {installed && !justSucceeded ? (
336
- <p className="mt-1 text-[10px] text-muted-foreground">Reviewable draft exists — see Definitions.</p>
337
- ) : null}
338
- </>
339
- );
340
- })()}
341
- </div>
342
- );
343
- })}
344
- </div>
345
- );
346
- }}
347
- </DataPanel>
348
- <Card>
349
- <CardHeader>
350
- <CardTitle className="flex items-center gap-2"><GitBranch className="h-4 w-4" /> Workflow graph</CardTitle>
351
- <CardDescription>See your saved workflows as a simple map.</CardDescription>
352
- </CardHeader>
353
- <CardContent>
354
- <div className="h-[440px] rounded-lg border border-border">
355
- <ReactFlow nodes={nodes} edges={edges} fitView>
356
- <Background />
357
- <Controls />
358
- </ReactFlow>
359
- </div>
360
- </CardContent>
361
- </Card>
362
- <DataPanel title="Definitions" result={defs.data}>
363
- {() => (
364
- <div className="space-y-3">
365
- <div className="grid gap-2 rounded-md border border-border p-3">
366
- <div className="flex flex-wrap gap-2">
367
- <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Workflow name" />
368
- <Button disabled={create.isPending} onClick={() => create.mutate()}>Create</Button>
369
- </div>
370
- <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste a workflow export" />
371
- <Button variant="outline" disabled={!importText.trim() || importWorkflow.isPending} onClick={() => importWorkflow.mutate()}>Import</Button>
372
- {create.data ? <OperationResult result={create.data} successLabel="Workflow created" /> : null}
373
- {importWorkflow.data ? <OperationResult result={importWorkflow.data} successLabel="Workflow imported" /> : null}
374
- </div>
375
- {workflows.length ? workflows.map((workflow) => {
376
- const id = String(workflow.id || workflow.workflow_id);
377
- return (
378
- <div key={id} className="rounded-md border border-border p-3">
379
- <div className="font-medium">{String(workflow.name || id)}</div>
380
- <div className="mt-2 flex gap-2">
381
- <ActionButton label="Run" action={() => latticeApi.runWorkflow(id)} invalidate={["workflowRuns"]} />
382
- <ActionButton label="Export" action={() => latticeApi.exportWorkflow(id)} />
383
- </div>
384
- </div>
385
- );
386
- }) : <EntityList items={[]} />}
387
- </div>
388
- )}
389
- </DataPanel>
390
- <DataPanel title="Automation triggers" result={triggers.data} className="xl:col-span-2">
391
- {(data) => <StructuredView value={data} />}
392
- </DataPanel>
393
- </div>
394
- );
395
- }
396
-
397
- function manualWorkflowNodes(): Array<Record<string, unknown>> {
398
- return [
399
- {
400
- id: "trigger",
401
- type: "trigger",
402
- name: "Manual start",
403
- config: { trigger: "manual" },
404
- next: "output",
405
- },
406
- {
407
- id: "output",
408
- type: "output",
409
- name: "Output",
410
- config: { value: "Workflow completed" },
411
- next: null,
412
- },
413
- ];
414
- }
415
-
416
- function HooksPanel() {
417
- const mode = useAppStore((state) => state.mode);
418
- const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
419
- const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
420
- if (mode === "basic") {
421
- return (
422
- <div className="grid gap-4 xl:grid-cols-2">
423
- <DataPanel title="Safeguards" result={hooks.data}>
424
- {(data) => <EntityList items={(data as Record<string, unknown>).hooks} titleKey="name" metaKey="kind" />}
425
- </DataPanel>
426
- <ModeGate title="Detailed hook logs" detail="Switch to Advanced when you need hook run logs and manual diagnostic controls." />
427
- </div>
428
- );
429
- }
430
- return (
431
- <div className="grid gap-4 xl:grid-cols-2">
432
- <DataPanel title="Hooks" result={hooks.data}>
433
- {(data) => <EntityList items={(data as Record<string, unknown>).hooks} titleKey="name" metaKey="kind" />}
434
- </DataPanel>
435
- <DataPanel title="Hook run log" result={runs.data}>
436
- {(data) => <EntityList items={(data as Record<string, unknown>).runs} titleKey="hook_id" metaKey="status" />}
437
- </DataPanel>
438
- <Card className="xl:col-span-2">
439
- <CardHeader>
440
- <CardTitle className="flex items-center gap-2"><PauseCircle className="h-4 w-4" /> Run manual hooks</CardTitle>
441
- <CardDescription>Trigger hooks deliberately and review the recorded result.</CardDescription>
442
- </CardHeader>
443
- <CardContent>
444
- <ActionButton label="Run all manual hooks" action={() => latticeApi.hookRun({ event: "manual" })} invalidate={["hookRuns"]} />
445
- </CardContent>
446
- </Card>
447
- </div>
448
- );
449
- }
450
-
451
- function ToolsPanel() {
452
- const tools = useQuery({ queryKey: ["toolPermissions"], queryFn: latticeApi.toolPermissions });
453
- return (
454
- <DataPanel title="Action permissions" result={tools.data}>
455
- {(data) => <EntityList items={(data as Record<string, unknown>).permissions || data} titleKey="tool" metaKey="risk" />}
456
- </DataPanel>
457
- );
458
- }
@@ -1,14 +0,0 @@
1
- import * as React from "react";
2
- import { type BrainState } from "@/components/LivingBrain";
3
- import { BrainHome } from "@/features/brain/BrainHome";
4
-
5
- export function AskPage() {
6
- const [brainPresence, setBrainPresence] = React.useState<{ state: BrainState; intensity: number }>({
7
- state: "idle",
8
- intensity: 0.58,
9
- });
10
- const setBrain = React.useCallback((state: BrainState, intensity = 0.58) => {
11
- setBrainPresence({ state, intensity });
12
- }, []);
13
- return <BrainHome brainState={brainPresence.state} intensity={brainPresence.intensity} onBrainChange={setBrain} />;
14
- }