ltcai 7.0.0 → 7.2.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.
- package/README.md +22 -20
- package/docs/CHANGELOG.md +43 -0
- package/frontend/src/App.tsx +80 -0
- package/frontend/src/api/client.ts +9 -0
- package/frontend/src/components/AdminAccessGate.tsx +70 -0
- package/frontend/src/components/BrainConversation.tsx +232 -8
- package/frontend/src/components/FeedbackState.tsx +45 -0
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
- package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
- package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
- package/frontend/src/features/admin/AdminConsole.tsx +41 -1
- package/frontend/src/features/brain/BrainConversation.tsx +232 -19
- package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
- package/frontend/src/features/brain/BrainHome.tsx +193 -1
- package/frontend/src/features/brain/brainData.ts +25 -1
- package/frontend/src/features/brain/graphLayout.ts +12 -1
- package/frontend/src/features/brain/types.ts +41 -0
- package/frontend/src/i18n.ts +328 -0
- package/frontend/src/store/appStore.ts +15 -0
- package/frontend/src/styles.css +1175 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/agent_runtime.py +51 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +12 -0
- package/latticeai/api/tools.py +14 -0
- package/latticeai/api/workspace.py +59 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/tool_registry.py +47 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/tool_dispatch.py +14 -0
- package/package.json +1 -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 +28 -28
- package/static/app/assets/Act-Di4tRFWY.js +2 -0
- package/static/app/assets/{Act-DhLaHnrT.js.map → Act-Di4tRFWY.js.map} +1 -1
- package/static/app/assets/Brain-BZB3Gy9w.js +322 -0
- package/static/app/assets/Brain-BZB3Gy9w.js.map +1 -0
- package/static/app/assets/{Capture-CAKouj52.js → Capture-tNyYWxnh.js} +2 -2
- package/static/app/assets/{Capture-CAKouj52.js.map → Capture-tNyYWxnh.js.map} +1 -1
- package/static/app/assets/Library-DAtDDLdg.js +2 -0
- package/static/app/assets/{Library-B_hW4AwR.js.map → Library-DAtDDLdg.js.map} +1 -1
- package/static/app/assets/System-DEu0xNUc.js +2 -0
- package/static/app/assets/System-DEu0xNUc.js.map +1 -0
- package/static/app/assets/{index-DKFpn5Kn.css → index-Bi_bpigM.css} +1 -1
- package/static/app/assets/index-COuGp7_5.js +17 -0
- package/static/app/assets/index-COuGp7_5.js.map +1 -0
- package/static/app/assets/primitives-CdwcE--L.js +2 -0
- package/static/app/assets/primitives-CdwcE--L.js.map +1 -0
- package/static/app/assets/textarea-CqOdBPL1.js +2 -0
- package/static/app/assets/{textarea-DR2Tyy_6.js.map → textarea-CqOdBPL1.js.map} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/Act-DhLaHnrT.js +0 -2
- package/static/app/assets/Brain-DS7NULyY.js +0 -322
- package/static/app/assets/Brain-DS7NULyY.js.map +0 -1
- package/static/app/assets/Library-B_hW4AwR.js +0 -2
- package/static/app/assets/System-9OAoXWPz.js +0 -2
- package/static/app/assets/System-9OAoXWPz.js.map +0 -1
- package/static/app/assets/index-C-7aHabu.js +0 -17
- package/static/app/assets/index-C-7aHabu.js.map +0 -1
- package/static/app/assets/primitives-DcO2H3yh.js +0 -2
- package/static/app/assets/primitives-DcO2H3yh.js.map +0 -1
- package/static/app/assets/textarea-DR2Tyy_6.js +0 -2
package/README.md
CHANGED
|
@@ -200,26 +200,26 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
|
|
|
200
200
|
|
|
201
201
|
## Current Release
|
|
202
202
|
|
|
203
|
-
The current release is **7.
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
|
|
207
|
-
-
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Expected artifacts for 7.
|
|
217
|
-
|
|
218
|
-
- `dist/ltcai-7.
|
|
219
|
-
- `dist/ltcai-7.
|
|
220
|
-
- `ltcai-7.
|
|
221
|
-
- `dist/ltcai-7.
|
|
222
|
-
- `src-tauri/target/release/bundle/dmg/Lattice AI_7.
|
|
203
|
+
The current release is **7.2.0 Runtime Trust Baseline**:
|
|
204
|
+
|
|
205
|
+
- Agent runs can be previewed before execution, including selected roles,
|
|
206
|
+
clamped retry budget, readiness state, and blocking reasons.
|
|
207
|
+
- Product agent execution refuses simulation-mode success unless simulation is
|
|
208
|
+
explicitly enabled for a non-product runtime.
|
|
209
|
+
- ToolRegistry exposes a live manifest and diagnostics endpoint covering
|
|
210
|
+
dispatch handlers, governance policy, catalog descriptions, and permissions.
|
|
211
|
+
- Tool governance/catalog drift is covered by unit tests so tool additions
|
|
212
|
+
cannot silently bypass policy or disappear from operator views.
|
|
213
|
+
- 7.1.0 Brain usability work remains intact while the runtime gains clearer
|
|
214
|
+
contracts for admin surfaces and future AgentRuntime extraction.
|
|
215
|
+
|
|
216
|
+
Expected artifacts for 7.2.0 release must use exact filenames:
|
|
217
|
+
|
|
218
|
+
- `dist/ltcai-7.2.0-py3-none-any.whl`
|
|
219
|
+
- `dist/ltcai-7.2.0.tar.gz`
|
|
220
|
+
- `ltcai-7.2.0.tgz`
|
|
221
|
+
- `dist/ltcai-7.2.0.vsix`
|
|
222
|
+
- `src-tauri/target/release/bundle/dmg/Lattice AI_7.2.0_aarch64.dmg`
|
|
223
223
|
|
|
224
224
|
Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
225
225
|
|
|
@@ -238,6 +238,8 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
|
|
|
238
238
|
|
|
239
239
|
| Version | Theme |
|
|
240
240
|
| --- | --- |
|
|
241
|
+
| 7.2.0 | Runtime Trust Baseline: agent run preview/readiness, simulation-mode guardrails, live ToolRegistry manifest/diagnostics, and tests for dispatch/governance/catalog drift |
|
|
242
|
+
| 7.1.0 | Brain Usability Completion: clearer first-run onboarding, ingestion progress/emergence, richer graph controls, inline answer proof, workspace/profile/admin discovery, empty/error/consent feedback, and VS Code sync status |
|
|
241
243
|
| 7.0.0 | Brain Productization Loop: first-screen ingestion for files/folders/notes/web, answer-level memory proof and source citations, model-continuity demo flow, five-minute first-run loop, and recall/KG quality eval in CI |
|
|
242
244
|
| 6.7.0 | Brain IA Cleanup: reachable rich pages, separated product routes vs compatibility aliases, shared Brain shell navigation, and lazy-loaded rich pages |
|
|
243
245
|
| 6.6.0 | Brain Proof Runtime: backend-owned Brain proof API, model-continuity wiring, first-screen proof that saved context can be recalled across model changes, and direct Brain Home document upload |
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ The top entry is either the current unreleased main-branch work or the current
|
|
|
4
4
|
release line. Older entries are historical and may describe behavior as it
|
|
5
5
|
existed at that release.
|
|
6
6
|
|
|
7
|
+
## [7.2.0] - 2026-06-20
|
|
8
|
+
|
|
9
|
+
> Runtime Trust Baseline. Adds execution preview and registry diagnostics so
|
|
10
|
+
> agent runs and tool permissions become inspectable contracts before action.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `POST /agents/api/run/preview` for AgentRuntime readiness, role selection,
|
|
14
|
+
input keys, retry clamping, and blocking reasons without starting a run.
|
|
15
|
+
- `GET /tools/registry` for the live ToolRegistry manifest across dispatch
|
|
16
|
+
handlers, governance policy, catalog descriptions, and permissions.
|
|
17
|
+
- `GET /tools/registry/diagnostics` for a compact drift check suitable for CI,
|
|
18
|
+
admin views, and runtime health panels.
|
|
19
|
+
- Unit coverage for AgentRuntime preview and ToolRegistry manifest diagnostics.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Tool governance now covers `read_document`, and the catalog describes
|
|
23
|
+
`create_web_project`, closing the current dispatch/governance/catalog drift.
|
|
24
|
+
- Package/runtime/static metadata is synchronized to 7.2.0; package publish and
|
|
25
|
+
deployment remain owner-run only.
|
|
26
|
+
|
|
27
|
+
## [7.1.0] - 2026-06-20
|
|
28
|
+
|
|
29
|
+
> Brain Usability Completion. Completes the 7.1.0 first-run through editor-sync
|
|
30
|
+
> usability pass: clear onboarding, visible ingestion, graph controls, inline
|
|
31
|
+
> proof, workspace/admin discovery, feedback states, and VS Code sync status.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
- Hardware visualization, expected timing, install timeline, and next-action
|
|
35
|
+
copy in first-run onboarding.
|
|
36
|
+
- Brain Home ingestion stage disclosure and memory emergence timeline for file,
|
|
37
|
+
folder, note, and URL sources.
|
|
38
|
+
- Knowledge Graph search suggestions, entity type filters, recent/all-time time
|
|
39
|
+
exploration, focus clearing, and neighbor highlighting.
|
|
40
|
+
- Inline answer citation markers with keyboard-accessible proof cards.
|
|
41
|
+
- Workspace/profile switcher, Admin Console gate, consent revoke feedback, and
|
|
42
|
+
shared empty/error feedback surfaces in the Brain shell.
|
|
43
|
+
- VS Code extension heartbeat/status endpoint plus extension and main-app sync
|
|
44
|
+
indicators for connected/indexing/synced/offline states.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- Package/runtime/static metadata is synchronized to 7.1.0; package publish and
|
|
48
|
+
deployment remain owner-run only.
|
|
49
|
+
|
|
7
50
|
## [7.0.0] - 2026-06-18
|
|
8
51
|
|
|
9
52
|
> Brain Productization Loop. Turns the Brain proof work into a first-five-minute
|
package/frontend/src/App.tsx
CHANGED
|
@@ -5,6 +5,12 @@ import { useAppStore } from "@/store/appStore";
|
|
|
5
5
|
import { parseHash, productShellRoutes } from "@/routes";
|
|
6
6
|
import { BrainHome } from "@/features/brain/BrainHome";
|
|
7
7
|
import { AdminConsole } from "@/features/admin/AdminConsole";
|
|
8
|
+
import { WorkspaceProfileSwitcher } from "@/components/WorkspaceProfileSwitcher";
|
|
9
|
+
import { AdminAccessGate } from "@/components/AdminAccessGate";
|
|
10
|
+
import { t, type Language } from "@/i18n";
|
|
11
|
+
import { latticeApi } from "@/api/client";
|
|
12
|
+
import { useQuery } from "@tanstack/react-query";
|
|
13
|
+
import { FeedbackState } from "@/components/FeedbackState";
|
|
8
14
|
|
|
9
15
|
const ActPage = React.lazy(() => import("@/pages/Act").then((module) => ({ default: module.ActPage })));
|
|
10
16
|
const BrainPage = React.lazy(() => import("@/pages/Brain").then((module) => ({ default: module.BrainPage })));
|
|
@@ -95,6 +101,7 @@ function BrainShell({
|
|
|
95
101
|
active: string;
|
|
96
102
|
children: React.ReactNode;
|
|
97
103
|
}) {
|
|
104
|
+
const language = useAppStore((state) => state.language);
|
|
98
105
|
return (
|
|
99
106
|
<main className="brain-shell-page" aria-label="Lattice workspace">
|
|
100
107
|
<nav className="brain-shell-nav" aria-label="Brain workspace navigation">
|
|
@@ -103,19 +110,92 @@ function BrainShell({
|
|
|
103
110
|
key={item.id}
|
|
104
111
|
type="button"
|
|
105
112
|
className={item.id === active ? "is-active" : ""}
|
|
113
|
+
aria-current={item.id === active ? "page" : undefined}
|
|
106
114
|
onClick={() => navigateHash(`/${item.path}`)}
|
|
107
115
|
>
|
|
108
116
|
{item.label}
|
|
109
117
|
</button>
|
|
110
118
|
))}
|
|
119
|
+
<div className="brain-shell-switchers" aria-label={t(language, "shell.workspace.label")}>
|
|
120
|
+
<VsCodeSyncStatus language={language} />
|
|
121
|
+
<WorkspaceProfileSwitcher language={language} />
|
|
122
|
+
<AdminAccessGate language={language} />
|
|
123
|
+
</div>
|
|
111
124
|
</nav>
|
|
112
125
|
<section className="brain-shell-content">
|
|
126
|
+
<ExternalConsentStatus language={language} />
|
|
113
127
|
{children}
|
|
114
128
|
</section>
|
|
115
129
|
</main>
|
|
116
130
|
);
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
function ExternalConsentStatus({ language }: { language: Language }) {
|
|
134
|
+
const externalConsent = useAppStore((state) => state.externalConsent);
|
|
135
|
+
const setExternalConsent = useAppStore((state) => state.setExternalConsent);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<section className="external-consent-status" aria-label={t(language, "feedback.consent.aria")}>
|
|
139
|
+
<FeedbackState
|
|
140
|
+
tone={externalConsent ? "empty" : "error"}
|
|
141
|
+
language={language}
|
|
142
|
+
title={externalConsent ? t(language, "feedback.consent.activeTitle") : t(language, "feedback.consent.revokedTitle")}
|
|
143
|
+
body={externalConsent ? t(language, "feedback.consent.activeBody") : t(language, "feedback.consent.revokedBody")}
|
|
144
|
+
actionLabel={externalConsent ? t(language, "feedback.consent.revoke") : t(language, "feedback.consent.reenable")}
|
|
145
|
+
onAction={() => setExternalConsent(!externalConsent)}
|
|
146
|
+
/>
|
|
147
|
+
</section>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function VsCodeSyncStatus({ language }: { language: Language }) {
|
|
152
|
+
const bridge = useQuery({
|
|
153
|
+
queryKey: ["workspaceVscodeStatus"],
|
|
154
|
+
queryFn: latticeApi.workspaceVscodeStatus,
|
|
155
|
+
refetchInterval: 15000,
|
|
156
|
+
});
|
|
157
|
+
const index = useQuery({
|
|
158
|
+
queryKey: ["index"],
|
|
159
|
+
queryFn: latticeApi.indexStatus,
|
|
160
|
+
refetchInterval: 15000,
|
|
161
|
+
});
|
|
162
|
+
const data = bridge.data?.data as Record<string, unknown> | undefined;
|
|
163
|
+
const lastSeen = Number(data?.last_seen_ms || 0);
|
|
164
|
+
const connected = Boolean(data?.connected) || (lastSeen > 0 && Date.now() - lastSeen < 60000);
|
|
165
|
+
const indexData = index.data?.data as Record<string, unknown> | undefined;
|
|
166
|
+
const indexStatus = String(indexData?.status || indexData?.state || "");
|
|
167
|
+
const indexing = /index|build|running|pending/i.test(indexStatus);
|
|
168
|
+
const state = bridge.isLoading
|
|
169
|
+
? "checking"
|
|
170
|
+
: !connected
|
|
171
|
+
? "offline"
|
|
172
|
+
: indexing
|
|
173
|
+
? "indexing"
|
|
174
|
+
: "synced";
|
|
175
|
+
const labelKey = `shell.sync.${state}`;
|
|
176
|
+
const detail = connected
|
|
177
|
+
? t(language, "shell.sync.detail")
|
|
178
|
+
: t(language, "shell.sync.detailOffline");
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className={`vscode-sync-status is-${state}`}
|
|
184
|
+
aria-label={`${t(language, "shell.sync.label")}: ${t(language, labelKey)}`}
|
|
185
|
+
title={`${t(language, "shell.sync.label")}: ${t(language, labelKey)}\n${detail}`}
|
|
186
|
+
onClick={() => {
|
|
187
|
+
window.location.hash = "/settings";
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
<span className="vscode-sync-dot" aria-hidden="true" />
|
|
191
|
+
<span className="vscode-sync-copy">
|
|
192
|
+
<strong>{t(language, "shell.sync.label")}</strong>
|
|
193
|
+
<small>{t(language, labelKey)}</small>
|
|
194
|
+
</span>
|
|
195
|
+
</button>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
119
199
|
function PageLoader() {
|
|
120
200
|
return (
|
|
121
201
|
<div className="brain-shell-loader" role="status">
|
|
@@ -416,6 +416,12 @@ export const latticeApi = {
|
|
|
416
416
|
},
|
|
417
417
|
health: () => get("/health", {}),
|
|
418
418
|
workspaceOs: () => get("/workspace/os", { counts: {}, models: {}, workspace_registry: { workspaces: [] } }),
|
|
419
|
+
workspaceVscodeStatus: () => get("/workspace/vscode/status", {
|
|
420
|
+
connected: false,
|
|
421
|
+
last_seen_ms: 0,
|
|
422
|
+
status: "offline",
|
|
423
|
+
index_status: "unknown",
|
|
424
|
+
}),
|
|
419
425
|
indexStatus: () => get("/api/index/status", {}),
|
|
420
426
|
rebuildIndex: () => post("/api/index/rebuild", { full: false, include_nodes: true, include_chunks: true }, {}),
|
|
421
427
|
graph: () => get("/knowledge-graph/graph", { nodes: [], edges: [] }),
|
|
@@ -475,7 +481,10 @@ export const latticeApi = {
|
|
|
475
481
|
unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
|
|
476
482
|
embeddingsStatus: () => get("/api/embeddings/status", {}),
|
|
477
483
|
agentRuntime: () => get("/agents/api/runtime/status", { runtime: {}, agents: [], runs: [] }),
|
|
484
|
+
agentRunPreview: (goal: string, roles: string[] = []) => post("/agents/api/run/preview", { goal, roles }, { ready: false, blocking_reasons: [] }),
|
|
478
485
|
runAgent: (goal: string, roles: string[]) => post("/agents/api/run", { goal, roles }, {}),
|
|
486
|
+
toolRegistryDiagnostics: () => get("/tools/registry/diagnostics", { diagnostics: { ready: false } }),
|
|
487
|
+
toolRegistry: () => get("/tools/registry", { status: "unavailable", diagnostics: {}, tools: [] }),
|
|
479
488
|
agentRun: (id: string) => get(`/agents/api/runs/${encodeURIComponent(id)}`, {}),
|
|
480
489
|
stopAgentRun: (id: string) => post(`/agents/api/runs/${encodeURIComponent(id)}/stop`, {}, {}),
|
|
481
490
|
agentRegistry: () => get("/agents/api/registry", { agents: [] }),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ShieldCheck, Lock } from "lucide-react";
|
|
3
|
+
import { t, type Language } from "@/i18n";
|
|
4
|
+
import { useAppStore, type WorkspaceMode } from "@/store/appStore";
|
|
5
|
+
|
|
6
|
+
const MODES: WorkspaceMode[] = ["basic", "advanced", "admin"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Role-aware admin entry point for the Brain shell.
|
|
10
|
+
*
|
|
11
|
+
* Mode is the access proxy used across the app (SystemPage gates the admin
|
|
12
|
+
* tab and AdminPanel on `mode`). When the user is in advanced/admin mode we
|
|
13
|
+
* surface a discoverable Admin Console button plus a compact mode switcher.
|
|
14
|
+
* In basic mode the console is hidden behind an explained affordance that
|
|
15
|
+
* promotes the user to admin mode rather than failing silently.
|
|
16
|
+
*/
|
|
17
|
+
export function AdminAccessGate({ language }: { language: Language }) {
|
|
18
|
+
const mode = useAppStore((state) => state.mode);
|
|
19
|
+
const setMode = useAppStore((state) => state.setMode);
|
|
20
|
+
const canSeeAdmin = mode === "advanced" || mode === "admin";
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="admin-access-gate">
|
|
24
|
+
{canSeeAdmin ? (
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
className="admin-access-button"
|
|
28
|
+
title={t(language, "shell.admin.tooltip")}
|
|
29
|
+
aria-label={t(language, "shell.admin.open")}
|
|
30
|
+
onClick={() => {
|
|
31
|
+
window.location.hash = "/admin";
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<ShieldCheck className="h-3.5 w-3.5" aria-hidden="true" />
|
|
35
|
+
<span>{t(language, "shell.admin.label")}</span>
|
|
36
|
+
</button>
|
|
37
|
+
) : (
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
className="admin-access-button is-locked"
|
|
41
|
+
title={t(language, "shell.admin.needsMode")}
|
|
42
|
+
aria-label={t(language, "shell.admin.needsMode")}
|
|
43
|
+
onClick={() => setMode("admin")}
|
|
44
|
+
>
|
|
45
|
+
<Lock className="h-3.5 w-3.5" aria-hidden="true" />
|
|
46
|
+
<span>{t(language, "shell.admin.enable")}</span>
|
|
47
|
+
</button>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
className="mode-switcher-mini"
|
|
52
|
+
role="group"
|
|
53
|
+
aria-label={t(language, "shell.mode.label")}
|
|
54
|
+
title={t(language, "shell.mode.info")}
|
|
55
|
+
>
|
|
56
|
+
{MODES.map((item) => (
|
|
57
|
+
<button
|
|
58
|
+
key={item}
|
|
59
|
+
type="button"
|
|
60
|
+
className={mode === item ? "is-active" : ""}
|
|
61
|
+
aria-pressed={mode === item}
|
|
62
|
+
onClick={() => setMode(item)}
|
|
63
|
+
>
|
|
64
|
+
{t(language, `shell.mode.${item}`)}
|
|
65
|
+
</button>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -3,13 +3,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { ImagePlus, MessageSquare, Plus, Send, Trash2 } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
5
|
import { EmptyState, EntityList, SourceBadge, StructuredView } from "@/components/primitives";
|
|
6
|
+
import { FeedbackState } from "@/components/FeedbackState";
|
|
6
7
|
import { type BrainState, LivingBrain } from "@/components/LivingBrain";
|
|
7
8
|
import { Badge } from "@/components/ui/badge";
|
|
8
9
|
import { Button } from "@/components/ui/button";
|
|
9
10
|
import { Textarea } from "@/components/ui/textarea";
|
|
10
11
|
import { asArray } from "@/lib/utils";
|
|
12
|
+
import { t, type Language } from "@/i18n";
|
|
13
|
+
import { useAppStore } from "@/store/appStore";
|
|
11
14
|
|
|
12
|
-
type
|
|
15
|
+
type Citation = { id: string; source: string; title: string; snippet: string; score: number | null };
|
|
16
|
+
type Msg = { role?: string; content?: string; timestamp?: string; citations?: Citation[] };
|
|
13
17
|
type BrainActivity = BrainState;
|
|
14
18
|
type BrainVitals = {
|
|
15
19
|
connected: boolean;
|
|
@@ -49,7 +53,58 @@ function usageNumber(data: unknown, key: string) {
|
|
|
49
53
|
return Number.isFinite(value) ? value : null;
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// Pull human-readable citation records out of the heterogeneous streaming trace.
|
|
57
|
+
// The backend trace shape varies, so we defensively scan the common containers
|
|
58
|
+
// (matches / sources / retrieved / context / chunks) for source-like records.
|
|
59
|
+
function extractCitations(trace: unknown): Citation[] {
|
|
60
|
+
if (!trace || typeof trace !== "object") return [];
|
|
61
|
+
const record = trace as Record<string, unknown>;
|
|
62
|
+
const containerKeys = ["matches", "sources", "retrieved", "context", "chunks", "citations", "evidence"];
|
|
63
|
+
let rows: Array<Record<string, unknown>> = [];
|
|
64
|
+
for (const key of containerKeys) {
|
|
65
|
+
const candidate = asArray<Record<string, unknown>>(record[key]);
|
|
66
|
+
if (candidate.length) {
|
|
67
|
+
rows = candidate;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!rows.length && Array.isArray(trace)) rows = trace as Array<Record<string, unknown>>;
|
|
72
|
+
|
|
73
|
+
const seen = new Set<string>();
|
|
74
|
+
const citations: Citation[] = [];
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
if (!row || typeof row !== "object") continue;
|
|
77
|
+
const title = String(row.title || row.name || row.heading || row.label || row.id || "").trim();
|
|
78
|
+
const source = String(row.source || row.type || row.origin || row.kind || row.provider || "memory").trim();
|
|
79
|
+
const snippet = String(row.snippet || row.text || row.content || row.summary || row.excerpt || "").trim();
|
|
80
|
+
if (!title && !snippet) continue;
|
|
81
|
+
const scoreRaw = Number(row.score ?? row.relevance ?? row.similarity);
|
|
82
|
+
const score = Number.isFinite(scoreRaw) ? Math.round((scoreRaw <= 1 ? scoreRaw * 100 : scoreRaw)) : null;
|
|
83
|
+
const id = String(row.id || `${source}:${title}:${snippet.slice(0, 24)}`);
|
|
84
|
+
if (seen.has(id)) continue;
|
|
85
|
+
seen.add(id);
|
|
86
|
+
citations.push({ id, source: source || "memory", title: title || snippet.slice(0, 48), snippet, score });
|
|
87
|
+
if (citations.length >= 6) break;
|
|
88
|
+
}
|
|
89
|
+
return citations;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Anchor citation chips after the first N sentences (conservative heuristic).
|
|
93
|
+
// Each segment carries an optional citation index so chips stay tied to text.
|
|
94
|
+
function splitWithCitations(content: string, citationCount: number): Array<{ text: string; citation: number | null }> {
|
|
95
|
+
if (!citationCount || !content) return [{ text: content, citation: null }];
|
|
96
|
+
const sentences = content.match(/[^.!?。!?\n]+[.!?。!?]?\s*/g);
|
|
97
|
+
if (!sentences || sentences.length <= 1) return [{ text: content, citation: 0 }];
|
|
98
|
+
const segments: Array<{ text: string; citation: number | null }> = [];
|
|
99
|
+
sentences.forEach((sentence, index) => {
|
|
100
|
+
const citation = index < citationCount ? index : null;
|
|
101
|
+
segments.push({ text: sentence, citation });
|
|
102
|
+
});
|
|
103
|
+
return segments;
|
|
104
|
+
}
|
|
105
|
+
|
|
52
106
|
export function BrainConversation({ className }: { className?: string }) {
|
|
107
|
+
const language = useAppStore((state) => state.language);
|
|
53
108
|
const qc = useQueryClient();
|
|
54
109
|
const history = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
|
|
55
110
|
const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
@@ -94,7 +149,20 @@ export function BrainConversation({ className }: { className?: string }) {
|
|
|
94
149
|
return next;
|
|
95
150
|
});
|
|
96
151
|
},
|
|
97
|
-
onTrace:
|
|
152
|
+
onTrace: (incoming) => {
|
|
153
|
+
setTrace(incoming);
|
|
154
|
+
const citations = extractCitations(incoming);
|
|
155
|
+
if (citations.length) {
|
|
156
|
+
setMessages((items) => {
|
|
157
|
+
const next = [...items];
|
|
158
|
+
const last = next[next.length - 1];
|
|
159
|
+
if (last && last.role === "assistant") {
|
|
160
|
+
next[next.length - 1] = { ...last, citations };
|
|
161
|
+
}
|
|
162
|
+
return next;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
},
|
|
98
166
|
},
|
|
99
167
|
);
|
|
100
168
|
if (result.error) {
|
|
@@ -167,7 +235,15 @@ export function BrainConversation({ className }: { className?: string }) {
|
|
|
167
235
|
<div key={`${msg.role || "message"}-${index}`} className={`brain-message-row ${msg.role === "user" ? "from-user" : "from-brain"}`}>
|
|
168
236
|
<div className="brain-message-bubble">
|
|
169
237
|
<div className="brain-message-role">{msg.role === "user" ? "You" : "Brain"}</div>
|
|
170
|
-
|
|
238
|
+
{msg.role === "user" ? (
|
|
239
|
+
<div className="whitespace-pre-wrap">{msg.content}</div>
|
|
240
|
+
) : (
|
|
241
|
+
<MessageWithCitations
|
|
242
|
+
language={language}
|
|
243
|
+
content={msg.content || ""}
|
|
244
|
+
citations={msg.citations || []}
|
|
245
|
+
/>
|
|
246
|
+
)}
|
|
171
247
|
</div>
|
|
172
248
|
</div>
|
|
173
249
|
)) : (
|
|
@@ -239,6 +315,122 @@ export function BrainConversation({ className }: { className?: string }) {
|
|
|
239
315
|
);
|
|
240
316
|
}
|
|
241
317
|
|
|
318
|
+
// Renders assistant text with inline, keyboard-accessible citation chips that are
|
|
319
|
+
// linked to a proof list below. Selecting a chip highlights and reveals its source.
|
|
320
|
+
function MessageWithCitations({
|
|
321
|
+
language,
|
|
322
|
+
content,
|
|
323
|
+
citations,
|
|
324
|
+
}: {
|
|
325
|
+
language: Language;
|
|
326
|
+
content: string;
|
|
327
|
+
citations: Citation[];
|
|
328
|
+
}) {
|
|
329
|
+
const [activeId, setActiveId] = React.useState<string | null>(null);
|
|
330
|
+
const [announce, setAnnounce] = React.useState("");
|
|
331
|
+
const listRef = React.useRef<HTMLOListElement | null>(null);
|
|
332
|
+
|
|
333
|
+
if (!citations.length) {
|
|
334
|
+
return <div className="whitespace-pre-wrap">{content}</div>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const segments = splitWithCitations(content, citations.length);
|
|
338
|
+
|
|
339
|
+
const focusListItem = (id: string) => {
|
|
340
|
+
const node = listRef.current?.querySelector<HTMLLIElement>(`[data-citation-target="${id}"]`);
|
|
341
|
+
node?.focus?.();
|
|
342
|
+
node?.scrollIntoView?.({ block: "nearest" });
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const toggle = (citation: Citation) => {
|
|
346
|
+
setActiveId((prev) => {
|
|
347
|
+
const next = prev === citation.id ? null : citation.id;
|
|
348
|
+
if (next) {
|
|
349
|
+
const idx = citations.findIndex((c) => c.id === citation.id) + 1;
|
|
350
|
+
setAnnounce(t(language, "brain.citation.announce.open", { num: idx, title: citation.title }));
|
|
351
|
+
focusListItem(citation.id);
|
|
352
|
+
} else {
|
|
353
|
+
setAnnounce(t(language, "brain.citation.announce.close"));
|
|
354
|
+
}
|
|
355
|
+
return next;
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const onChipKey = (event: React.KeyboardEvent<HTMLSpanElement>, citation: Citation) => {
|
|
360
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
361
|
+
event.preventDefault();
|
|
362
|
+
toggle(citation);
|
|
363
|
+
} else if (event.key === "Escape" && activeId) {
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
setActiveId(null);
|
|
366
|
+
setAnnounce(t(language, "brain.citation.announce.close"));
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div className="brain-message-cited">
|
|
372
|
+
<div className="whitespace-pre-wrap brain-message-text">
|
|
373
|
+
{segments.map((segment, index) => {
|
|
374
|
+
const citation = segment.citation != null ? citations[segment.citation] : null;
|
|
375
|
+
const num = segment.citation != null ? segment.citation + 1 : 0;
|
|
376
|
+
return (
|
|
377
|
+
<React.Fragment key={index}>
|
|
378
|
+
{segment.text}
|
|
379
|
+
{citation ? (
|
|
380
|
+
<span
|
|
381
|
+
role="button"
|
|
382
|
+
tabIndex={0}
|
|
383
|
+
className={`citation-chip ${activeId === citation.id ? "is-active" : ""}`}
|
|
384
|
+
data-citation-id={citation.id}
|
|
385
|
+
aria-pressed={activeId === citation.id}
|
|
386
|
+
aria-label={t(language, "brain.citation.chip.long", { num, title: citation.title })}
|
|
387
|
+
onClick={() => toggle(citation)}
|
|
388
|
+
onKeyDown={(event) => onChipKey(event, citation)}
|
|
389
|
+
>
|
|
390
|
+
{t(language, "brain.citation.chip", { num })}
|
|
391
|
+
</span>
|
|
392
|
+
) : null}
|
|
393
|
+
</React.Fragment>
|
|
394
|
+
);
|
|
395
|
+
})}
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div className="brain-answer-proof" aria-label={t(language, "brain.citation.region")}>
|
|
399
|
+
<div className="brain-answer-proof-head">
|
|
400
|
+
<span>{t(language, "brain.answerProof.title")}</span>
|
|
401
|
+
<strong>{t(language, "brain.citation.count", { count: citations.length })}</strong>
|
|
402
|
+
</div>
|
|
403
|
+
<ol ref={listRef}>
|
|
404
|
+
{citations.map((citation, index) => {
|
|
405
|
+
const isActive = activeId === citation.id;
|
|
406
|
+
return (
|
|
407
|
+
<li
|
|
408
|
+
key={citation.id}
|
|
409
|
+
data-citation-target={citation.id}
|
|
410
|
+
className={isActive ? "is-cited" : ""}
|
|
411
|
+
tabIndex={-1}
|
|
412
|
+
aria-current={isActive ? "true" : undefined}
|
|
413
|
+
>
|
|
414
|
+
<span>{t(language, "brain.citation.preview.from")}{citation.source}</span>
|
|
415
|
+
<strong>{t(language, "brain.citation.chip", { num: index + 1 })} · {citation.title}</strong>
|
|
416
|
+
{citation.snippet ? (
|
|
417
|
+
<small>{t(language, "brain.citation.preview.snippet")}{citation.snippet}</small>
|
|
418
|
+
) : null}
|
|
419
|
+
{citation.score != null ? (
|
|
420
|
+
<small className="brain-citation-score">{t(language, "brain.citation.score", { score: citation.score })}</small>
|
|
421
|
+
) : null}
|
|
422
|
+
</li>
|
|
423
|
+
);
|
|
424
|
+
})}
|
|
425
|
+
</ol>
|
|
426
|
+
<small className="brain-citation-tip">{t(language, "brain.citation.keyboard.tip")}</small>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
429
|
+
<div className="sr-only" role="status" aria-live="polite">{announce}</div>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
242
434
|
function RecentConversations({
|
|
243
435
|
conversations,
|
|
244
436
|
result,
|
|
@@ -248,23 +440,32 @@ function RecentConversations({
|
|
|
248
440
|
onDelete,
|
|
249
441
|
}: {
|
|
250
442
|
conversations: Array<Record<string, unknown>>;
|
|
251
|
-
result?:
|
|
443
|
+
result?: { source?: string; ok?: boolean; status?: number; error?: string };
|
|
252
444
|
activeId: string | null;
|
|
253
445
|
onNew: () => void;
|
|
254
446
|
onSelect: (id: string) => void;
|
|
255
447
|
onDelete: (id: string) => void;
|
|
256
448
|
}) {
|
|
449
|
+
const language = useAppStore((state) => state.language);
|
|
257
450
|
return (
|
|
258
451
|
<section className="brain-side-panel">
|
|
259
452
|
<div className="brain-side-head">
|
|
260
453
|
<div>
|
|
261
454
|
<h3>Recent conversations</h3>
|
|
262
|
-
<SourceBadge result={result} />
|
|
455
|
+
<SourceBadge result={result as Parameters<typeof SourceBadge>[0]["result"]} />
|
|
263
456
|
</div>
|
|
264
457
|
<Button variant="outline" size="sm" onClick={onNew}><Plus className="h-4 w-4" /> New</Button>
|
|
265
458
|
</div>
|
|
266
459
|
<div className="brain-conversation-list soft-scrollbar">
|
|
267
|
-
{
|
|
460
|
+
{!result?.ok && result?.source === "unavailable" ? (
|
|
461
|
+
<FeedbackState
|
|
462
|
+
tone="error"
|
|
463
|
+
language={language}
|
|
464
|
+
title={t(language, "feedback.error.title")}
|
|
465
|
+
body={result.error || t(language, "feedback.error.body")}
|
|
466
|
+
onAction={() => window.location.reload()}
|
|
467
|
+
/>
|
|
468
|
+
) : conversations.length ? conversations.slice(0, 8).map((item) => {
|
|
268
469
|
const id = String(item.id || item.conversation_id || "");
|
|
269
470
|
return (
|
|
270
471
|
<div key={id} className={`brain-conversation-item ${activeId === id ? "is-active" : ""}`}>
|
|
@@ -277,13 +478,23 @@ function RecentConversations({
|
|
|
277
478
|
</button>
|
|
278
479
|
</div>
|
|
279
480
|
);
|
|
280
|
-
}) :
|
|
481
|
+
}) : (
|
|
482
|
+
<FeedbackState
|
|
483
|
+
tone="empty"
|
|
484
|
+
language={language}
|
|
485
|
+
title="No conversations yet"
|
|
486
|
+
body="New exchanges will appear here."
|
|
487
|
+
actionLabel="Start a conversation"
|
|
488
|
+
onAction={onNew}
|
|
489
|
+
/>
|
|
490
|
+
)}
|
|
281
491
|
</div>
|
|
282
492
|
</section>
|
|
283
493
|
);
|
|
284
494
|
}
|
|
285
495
|
|
|
286
496
|
function MemoryNearby({ question, trace }: { question: string; trace: unknown }) {
|
|
497
|
+
const language = useAppStore((state) => state.language);
|
|
287
498
|
const hybrid = useQuery({
|
|
288
499
|
queryKey: ["brainNearbyMemory", question],
|
|
289
500
|
queryFn: () => latticeApi.hybridSearch(question),
|
|
@@ -299,10 +510,23 @@ function MemoryNearby({ question, trace }: { question: string; trace: unknown })
|
|
|
299
510
|
</div>
|
|
300
511
|
{hybrid.data?.ok ? (
|
|
301
512
|
<EntityList items={(hybrid.data.data as Record<string, unknown>).matches || hybrid.data.data} titleKey="title" metaKey="type" limit={4} />
|
|
513
|
+
) : hybrid.data?.source === "unavailable" ? (
|
|
514
|
+
<FeedbackState
|
|
515
|
+
tone="error"
|
|
516
|
+
language={language}
|
|
517
|
+
title={t(language, "feedback.error.title")}
|
|
518
|
+
body={hybrid.data.error || t(language, "feedback.error.body")}
|
|
519
|
+
onAction={() => void hybrid.refetch()}
|
|
520
|
+
/>
|
|
302
521
|
) : trace ? (
|
|
303
522
|
<StructuredView value={trace} limit={4} />
|
|
304
523
|
) : (
|
|
305
|
-
<
|
|
524
|
+
<FeedbackState
|
|
525
|
+
tone="empty"
|
|
526
|
+
language={language}
|
|
527
|
+
title="Quiet for now"
|
|
528
|
+
body="Relevant memory wakes up as the conversation gets specific."
|
|
529
|
+
/>
|
|
306
530
|
)}
|
|
307
531
|
</section>
|
|
308
532
|
);
|