ltcai 6.7.0 → 7.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.
- package/README.md +29 -31
- package/docs/CHANGELOG.md +48 -0
- package/frontend/src/App.tsx +80 -0
- package/frontend/src/api/client.ts +11 -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/brain/BrainConversation.tsx +382 -7
- package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
- package/frontend/src/features/brain/BrainHome.tsx +300 -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 +54 -0
- package/frontend/src/i18n.ts +374 -0
- package/frontend/src/store/appStore.ts +15 -0
- package/frontend/src/styles.css +1383 -41
- 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/workspace.py +59 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +1 -1
- package/scripts/brain_quality_eval.py +118 -0
- 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-BHEb8bAM.js +2 -0
- package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
- package/static/app/assets/Brain-yvR7xkrV.js +322 -0
- package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
- package/static/app/assets/Capture-BrAmsSEH.js +2 -0
- package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
- package/static/app/assets/Library-BOVzYfxI.js +2 -0
- package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
- package/static/app/assets/System-D4WaN4kj.js +2 -0
- package/static/app/assets/System-D4WaN4kj.js.map +1 -0
- package/static/app/assets/index-BOB-W1FZ.js +17 -0
- package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
- package/static/app/assets/index-DvLsGy-z.css +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
- package/static/app/assets/textarea-CbvhHvzg.js +2 -0
- package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/Act-DGN37eR4.js +0 -2
- package/static/app/assets/Brain-HDcIwPHW.js +0 -322
- package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
- package/static/app/assets/Capture--eZfb_Ex.js +0 -2
- package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
- package/static/app/assets/Library-BbagRFyd.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
- package/static/app/assets/index-C950aES_.js +0 -17
- package/static/app/assets/index-C950aES_.js.map +0 -1
- package/static/app/assets/index-ztppMg0c.css +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
- package/static/app/assets/textarea-C2PLu6_O.js +0 -2
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Cpu, DatabaseZap, FileUp, Search, Settings, ShieldCheck } from "lucide-react";
|
|
2
|
+
import { Cpu, DatabaseZap, FileText, FileUp, FolderPlus, Globe2, Loader2, Repeat2, Search, Settings, ShieldCheck, Sparkles } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
const INGESTION_TYPE_LABEL_KEY: Record<IngestionSourceType, string> = {
|
|
5
|
+
file: "brain.ingest.type.file",
|
|
6
|
+
folder: "brain.ingest.type.folder",
|
|
7
|
+
note: "brain.ingest.type.note",
|
|
8
|
+
web: "brain.ingest.type.web",
|
|
9
|
+
};
|
|
3
10
|
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
|
4
11
|
import { t, type Language } from "@/i18n";
|
|
5
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
INGESTION_STAGE_ORDER,
|
|
14
|
+
type BrainDepth,
|
|
15
|
+
type BrainProof,
|
|
16
|
+
type BrainReadiness,
|
|
17
|
+
type EmergenceEvent,
|
|
18
|
+
type IngestionPipelineStage,
|
|
19
|
+
type IngestionSourceType,
|
|
20
|
+
type IngestionState,
|
|
21
|
+
type KnowledgeConcept,
|
|
22
|
+
type MemoryFragment,
|
|
23
|
+
type Message,
|
|
24
|
+
} from "./types";
|
|
6
25
|
import { BrainCarePanel } from "./BrainCarePanel";
|
|
7
26
|
import { BrainComposer } from "./BrainComposer";
|
|
8
27
|
import { BrainOverviewPanel } from "./BrainOverviewPanel";
|
|
@@ -14,6 +33,8 @@ export function BrainConversation({
|
|
|
14
33
|
messages,
|
|
15
34
|
starterPrompts,
|
|
16
35
|
memoryFeedback,
|
|
36
|
+
ingestionStates,
|
|
37
|
+
emergenceEvents,
|
|
17
38
|
draft,
|
|
18
39
|
streaming,
|
|
19
40
|
imageData,
|
|
@@ -27,6 +48,10 @@ export function BrainConversation({
|
|
|
27
48
|
onDraftChange,
|
|
28
49
|
onImageDataChange,
|
|
29
50
|
onUploadDocument,
|
|
51
|
+
onConnectFolder,
|
|
52
|
+
onIngestNote,
|
|
53
|
+
onIngestWeb,
|
|
54
|
+
onVerifyModelContinuity,
|
|
30
55
|
onSend,
|
|
31
56
|
}: {
|
|
32
57
|
language: Language;
|
|
@@ -35,6 +60,8 @@ export function BrainConversation({
|
|
|
35
60
|
messages: Message[];
|
|
36
61
|
starterPrompts: string[];
|
|
37
62
|
memoryFeedback: string | null;
|
|
63
|
+
ingestionStates: Record<IngestionSourceType, IngestionState | null>;
|
|
64
|
+
emergenceEvents: EmergenceEvent[];
|
|
38
65
|
draft: string;
|
|
39
66
|
streaming: boolean;
|
|
40
67
|
imageData: string | null;
|
|
@@ -48,6 +75,10 @@ export function BrainConversation({
|
|
|
48
75
|
onDraftChange: (value: string) => void;
|
|
49
76
|
onImageDataChange: (value: string | null) => void;
|
|
50
77
|
onUploadDocument: (file: File) => void;
|
|
78
|
+
onConnectFolder: (path: string) => void;
|
|
79
|
+
onIngestNote: (note: string) => void;
|
|
80
|
+
onIngestWeb: (url: string) => void;
|
|
81
|
+
onVerifyModelContinuity: () => void;
|
|
51
82
|
onSend: () => void;
|
|
52
83
|
}) {
|
|
53
84
|
return (
|
|
@@ -89,6 +120,16 @@ export function BrainConversation({
|
|
|
89
120
|
</div>
|
|
90
121
|
|
|
91
122
|
<div ref={streamRef} className="brain-stream">
|
|
123
|
+
<BrainIngestionPanel
|
|
124
|
+
language={language}
|
|
125
|
+
uploadingDocument={uploadingDocument}
|
|
126
|
+
ingestionStates={ingestionStates}
|
|
127
|
+
onUploadDocument={onUploadDocument}
|
|
128
|
+
onConnectFolder={onConnectFolder}
|
|
129
|
+
onIngestNote={onIngestNote}
|
|
130
|
+
onIngestWeb={onIngestWeb}
|
|
131
|
+
/>
|
|
132
|
+
<IngestionTimelineSection language={language} emergenceEvents={emergenceEvents} />
|
|
92
133
|
<BrainOverviewPanel
|
|
93
134
|
memories={memories}
|
|
94
135
|
concepts={concepts}
|
|
@@ -96,6 +137,12 @@ export function BrainConversation({
|
|
|
96
137
|
proof={proof}
|
|
97
138
|
onOpenDepth={onOpenDepth}
|
|
98
139
|
/>
|
|
140
|
+
<ModelContinuityDemo
|
|
141
|
+
language={language}
|
|
142
|
+
proof={proof}
|
|
143
|
+
modelName={modelName}
|
|
144
|
+
onVerify={onVerifyModelContinuity}
|
|
145
|
+
/>
|
|
99
146
|
{messages.length === 0 ? (
|
|
100
147
|
<BrainEmptyState
|
|
101
148
|
language={language}
|
|
@@ -105,11 +152,21 @@ export function BrainConversation({
|
|
|
105
152
|
onUploadDocument={onUploadDocument}
|
|
106
153
|
/>
|
|
107
154
|
) : (
|
|
108
|
-
messages.map((message, index) =>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
messages.map((message, index) => {
|
|
156
|
+
const messageId = `brain-msg-${index}`;
|
|
157
|
+
const proof = message.role === "assistant" ? message.proof : undefined;
|
|
158
|
+
return (
|
|
159
|
+
<div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
|
|
160
|
+
<div className="brain-message-bubble">
|
|
161
|
+
{message.content}
|
|
162
|
+
{proof && proof.citations.length ? (
|
|
163
|
+
<InlineCitationMarkers language={language} proof={proof} messageId={messageId} />
|
|
164
|
+
) : null}
|
|
165
|
+
</div>
|
|
166
|
+
{proof ? <AnswerProofCard language={language} proof={proof} messageId={messageId} /> : null}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
})
|
|
113
170
|
)}
|
|
114
171
|
</div>
|
|
115
172
|
|
|
@@ -138,6 +195,324 @@ export function BrainConversation({
|
|
|
138
195
|
);
|
|
139
196
|
}
|
|
140
197
|
|
|
198
|
+
function BrainIngestionPanel({
|
|
199
|
+
language,
|
|
200
|
+
uploadingDocument,
|
|
201
|
+
ingestionStates,
|
|
202
|
+
onUploadDocument,
|
|
203
|
+
onConnectFolder,
|
|
204
|
+
onIngestNote,
|
|
205
|
+
onIngestWeb,
|
|
206
|
+
}: {
|
|
207
|
+
language: Language;
|
|
208
|
+
uploadingDocument: boolean;
|
|
209
|
+
ingestionStates: Record<IngestionSourceType, IngestionState | null>;
|
|
210
|
+
onUploadDocument: (file: File) => void;
|
|
211
|
+
onConnectFolder: (path: string) => void;
|
|
212
|
+
onIngestNote: (note: string) => void;
|
|
213
|
+
onIngestWeb: (url: string) => void;
|
|
214
|
+
}) {
|
|
215
|
+
const [folderPath, setFolderPath] = React.useState("");
|
|
216
|
+
const [note, setNote] = React.useState("");
|
|
217
|
+
const [url, setUrl] = React.useState("");
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<section className="brain-ingestion-panel" aria-label={t(language, "brain.ingest.aria")}>
|
|
221
|
+
<div className="brain-ingestion-head">
|
|
222
|
+
<span>{t(language, "brain.ingest.kicker")}</span>
|
|
223
|
+
<strong>{t(language, "brain.ingest.title")}</strong>
|
|
224
|
+
</div>
|
|
225
|
+
<div className="brain-ingestion-grid">
|
|
226
|
+
<label
|
|
227
|
+
className={`brain-ingest-tile is-primary ${tileStateClass(ingestionStates.file)} ${uploadingDocument ? "is-disabled" : ""}`}
|
|
228
|
+
>
|
|
229
|
+
<FileUp className="h-4 w-4" />
|
|
230
|
+
<span>{uploadingDocument ? t(language, "brain.upload.uploading") : t(language, "brain.ingest.file")}</span>
|
|
231
|
+
<small>{t(language, "brain.ingest.file.detail")}</small>
|
|
232
|
+
<input
|
|
233
|
+
type="file"
|
|
234
|
+
accept=".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,application/pdf,text/plain,text/markdown,text/csv"
|
|
235
|
+
className="sr-only"
|
|
236
|
+
disabled={uploadingDocument}
|
|
237
|
+
onChange={(event) => {
|
|
238
|
+
const file = event.target.files?.[0];
|
|
239
|
+
event.currentTarget.value = "";
|
|
240
|
+
if (file) onUploadDocument(file);
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
<IngestionStageTrack language={language} state={ingestionStates.file} ctaKey="brain.ingest.cta.file" />
|
|
244
|
+
</label>
|
|
245
|
+
<form
|
|
246
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.folder)}`}
|
|
247
|
+
onSubmit={(event) => {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
onConnectFolder(folderPath);
|
|
250
|
+
setFolderPath("");
|
|
251
|
+
}}
|
|
252
|
+
>
|
|
253
|
+
<FolderPlus className="h-4 w-4" />
|
|
254
|
+
<span>{t(language, "brain.ingest.folder")}</span>
|
|
255
|
+
<input value={folderPath} onChange={(event) => setFolderPath(event.target.value)} placeholder={t(language, "brain.ingest.folder.placeholder")} />
|
|
256
|
+
<IngestionStageTrack language={language} state={ingestionStates.folder} ctaKey="brain.ingest.cta.folder" />
|
|
257
|
+
</form>
|
|
258
|
+
<form
|
|
259
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.note)}`}
|
|
260
|
+
onSubmit={(event) => {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
onIngestNote(note);
|
|
263
|
+
setNote("");
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
<FileText className="h-4 w-4" />
|
|
267
|
+
<span>{t(language, "brain.ingest.note")}</span>
|
|
268
|
+
<input value={note} onChange={(event) => setNote(event.target.value)} placeholder={t(language, "brain.ingest.note.placeholder")} />
|
|
269
|
+
<IngestionStageTrack language={language} state={ingestionStates.note} ctaKey="brain.ingest.cta.note" />
|
|
270
|
+
</form>
|
|
271
|
+
<form
|
|
272
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.web)}`}
|
|
273
|
+
onSubmit={(event) => {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
onIngestWeb(url);
|
|
276
|
+
setUrl("");
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<Globe2 className="h-4 w-4" />
|
|
280
|
+
<span>{t(language, "brain.ingest.web")}</span>
|
|
281
|
+
<input value={url} onChange={(event) => setUrl(event.target.value)} placeholder={t(language, "brain.ingest.web.placeholder")} />
|
|
282
|
+
<IngestionStageTrack language={language} state={ingestionStates.web} ctaKey="brain.ingest.cta.web" />
|
|
283
|
+
</form>
|
|
284
|
+
</div>
|
|
285
|
+
</section>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function tileStateClass(state: IngestionState | null): string {
|
|
290
|
+
if (!state) return "";
|
|
291
|
+
if (state.stage === "error") return "is-failed";
|
|
292
|
+
if (state.stage === "complete") return "is-emerged";
|
|
293
|
+
return "is-ingesting";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const STAGE_HINT_KEY: Record<IngestionPipelineStage, string> = {
|
|
297
|
+
preparing: "brain.ingest.stage.preparing.hint",
|
|
298
|
+
parsing: "brain.ingest.stage.parsing.hint",
|
|
299
|
+
embedding: "brain.ingest.stage.embedding.hint",
|
|
300
|
+
indexing: "brain.ingest.stage.indexing.hint",
|
|
301
|
+
complete: "brain.ingest.stage.complete.hint",
|
|
302
|
+
error: "brain.ingest.stage.error",
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// Progressive disclosure of the pipeline: collect -> parse -> embed -> memorize.
|
|
306
|
+
function IngestionStageTrack({
|
|
307
|
+
language,
|
|
308
|
+
state,
|
|
309
|
+
ctaKey,
|
|
310
|
+
}: {
|
|
311
|
+
language: Language;
|
|
312
|
+
state: IngestionState | null;
|
|
313
|
+
ctaKey: string;
|
|
314
|
+
}) {
|
|
315
|
+
if (!state) {
|
|
316
|
+
return <small className="brain-ingest-cta">{t(language, ctaKey)}</small>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const activeIndex = INGESTION_STAGE_ORDER.indexOf(state.stage);
|
|
320
|
+
const isError = state.stage === "error";
|
|
321
|
+
const isComplete = state.stage === "complete";
|
|
322
|
+
const hasEmergence = state.newMemories > 0 || state.newEntities > 0;
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
className="brain-ingest-progress"
|
|
327
|
+
role="status"
|
|
328
|
+
aria-live="polite"
|
|
329
|
+
aria-label={t(language, "brain.ingest.progress.aria", { label: state.label })}
|
|
330
|
+
>
|
|
331
|
+
<div className="brain-ingest-stage-badges" aria-hidden="true">
|
|
332
|
+
{INGESTION_STAGE_ORDER.filter((stage) => stage !== "complete").map((stage) => {
|
|
333
|
+
const index = INGESTION_STAGE_ORDER.indexOf(stage);
|
|
334
|
+
const done = !isError && !isComplete && index < activeIndex;
|
|
335
|
+
const active = !isError && !isComplete && index === activeIndex;
|
|
336
|
+
const passed = isComplete;
|
|
337
|
+
const cls = isError
|
|
338
|
+
? "is-failed"
|
|
339
|
+
: passed || done
|
|
340
|
+
? "is-done"
|
|
341
|
+
: active
|
|
342
|
+
? "is-active"
|
|
343
|
+
: "";
|
|
344
|
+
return (
|
|
345
|
+
<span key={stage} className={`brain-ingest-stage-badge ${cls}`}>
|
|
346
|
+
{(active || (isError && index === activeIndex)) ? <Loader2 className="h-3 w-3 brain-ingest-spin" /> : null}
|
|
347
|
+
{t(language, `brain.ingest.stage.${stage}`)}
|
|
348
|
+
</span>
|
|
349
|
+
);
|
|
350
|
+
})}
|
|
351
|
+
</div>
|
|
352
|
+
<small className={`brain-ingest-stage-hint ${isError ? "is-failed" : isComplete ? "is-emerged" : ""}`}>
|
|
353
|
+
{isError
|
|
354
|
+
? t(language, "brain.ingest.stage.error")
|
|
355
|
+
: isComplete
|
|
356
|
+
? hasEmergence
|
|
357
|
+
? t(language, "brain.ingest.result", { memories: state.newMemories, entities: state.newEntities })
|
|
358
|
+
: t(language, "brain.ingest.result.empty")
|
|
359
|
+
: t(language, STAGE_HINT_KEY[state.stage])}
|
|
360
|
+
</small>
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function relativeTime(language: Language, at: number, now: number): string {
|
|
366
|
+
const diffMinutes = Math.floor((now - at) / 60000);
|
|
367
|
+
if (diffMinutes < 1) return t(language, "brain.timeline.justNow");
|
|
368
|
+
return t(language, "brain.timeline.minutesAgo", { count: diffMinutes });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function IngestionTimelineSection({
|
|
372
|
+
language,
|
|
373
|
+
emergenceEvents,
|
|
374
|
+
}: {
|
|
375
|
+
language: Language;
|
|
376
|
+
emergenceEvents: EmergenceEvent[];
|
|
377
|
+
}) {
|
|
378
|
+
// Re-read the clock on each render of the parent so relative labels stay fresh-enough.
|
|
379
|
+
const now = Date.now();
|
|
380
|
+
return (
|
|
381
|
+
<section
|
|
382
|
+
className="brain-emergence-timeline"
|
|
383
|
+
aria-label={t(language, "brain.timeline.aria")}
|
|
384
|
+
aria-live="polite"
|
|
385
|
+
>
|
|
386
|
+
<div className="brain-emergence-head">
|
|
387
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
388
|
+
<strong>{t(language, "brain.timeline.emergenceTitle")}</strong>
|
|
389
|
+
<span>{t(language, "brain.timeline.recent")}</span>
|
|
390
|
+
</div>
|
|
391
|
+
{emergenceEvents.length === 0 ? (
|
|
392
|
+
<p className="brain-emergence-empty">{t(language, "brain.timeline.empty")}</p>
|
|
393
|
+
) : (
|
|
394
|
+
<ol className="brain-emergence-list">
|
|
395
|
+
{emergenceEvents.map((event) => (
|
|
396
|
+
<li key={event.id} className="brain-emergence-item">
|
|
397
|
+
<span className="brain-emergence-type">{t(language, INGESTION_TYPE_LABEL_KEY[event.sourceType])}</span>
|
|
398
|
+
<span className="brain-emergence-label">{event.label}</span>
|
|
399
|
+
<span className="brain-emergence-counts">
|
|
400
|
+
<strong>{t(language, "brain.timeline.newMemories", { count: event.newMemories })}</strong>
|
|
401
|
+
<strong>{t(language, "brain.timeline.newEntities", { count: event.newEntities })}</strong>
|
|
402
|
+
</span>
|
|
403
|
+
<time className="brain-emergence-at">{relativeTime(language, event.at, now)}</time>
|
|
404
|
+
</li>
|
|
405
|
+
))}
|
|
406
|
+
</ol>
|
|
407
|
+
)}
|
|
408
|
+
</section>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function ModelContinuityDemo({
|
|
413
|
+
language,
|
|
414
|
+
proof,
|
|
415
|
+
modelName,
|
|
416
|
+
onVerify,
|
|
417
|
+
}: {
|
|
418
|
+
language: Language;
|
|
419
|
+
proof: BrainProof;
|
|
420
|
+
modelName: string;
|
|
421
|
+
onVerify: () => void;
|
|
422
|
+
}) {
|
|
423
|
+
return (
|
|
424
|
+
<section className="brain-model-demo" aria-label={t(language, "brain.modelDemo.aria")}>
|
|
425
|
+
<div>
|
|
426
|
+
<span>{t(language, "brain.modelDemo.kicker")}</span>
|
|
427
|
+
<strong>{proof.modelContinuity.proven ? t(language, "brain.modelDemo.proven") : t(language, "brain.modelDemo.pending")}</strong>
|
|
428
|
+
<small>{t(language, "brain.modelDemo.detail", { model: proof.modelContinuity.activeModel || modelName })}</small>
|
|
429
|
+
</div>
|
|
430
|
+
<button type="button" onClick={onVerify}>
|
|
431
|
+
<Repeat2 className="h-3.5 w-3.5" />
|
|
432
|
+
{t(language, "brain.modelDemo.verify")}
|
|
433
|
+
</button>
|
|
434
|
+
<button type="button" onClick={() => navigateHash("/models")}>
|
|
435
|
+
<Cpu className="h-3.5 w-3.5" />
|
|
436
|
+
{t(language, "brain.modelDemo.change")}
|
|
437
|
+
</button>
|
|
438
|
+
</section>
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function citationDomId(messageId: string, citationId: string): string {
|
|
443
|
+
return `${messageId}-cite-${citationId}`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Inline, keyboard-focusable citation markers rendered next to the answer text.
|
|
447
|
+
// Activating one moves focus to the matching row in the evidence card below.
|
|
448
|
+
function InlineCitationMarkers({
|
|
449
|
+
language,
|
|
450
|
+
proof,
|
|
451
|
+
messageId,
|
|
452
|
+
}: {
|
|
453
|
+
language: Language;
|
|
454
|
+
proof: NonNullable<Message["proof"]>;
|
|
455
|
+
messageId: string;
|
|
456
|
+
}) {
|
|
457
|
+
const focusCitation = (citationId: string) => {
|
|
458
|
+
const target = document.getElementById(citationDomId(messageId, citationId));
|
|
459
|
+
if (target) {
|
|
460
|
+
target.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
461
|
+
target.focus();
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
return (
|
|
465
|
+
<span className="brain-inline-citations" aria-label={t(language, "brain.answerProof.citationsLabel", { count: proof.citations.length })}>
|
|
466
|
+
{proof.citations.map((citation, index) => (
|
|
467
|
+
<button
|
|
468
|
+
key={citation.id}
|
|
469
|
+
type="button"
|
|
470
|
+
className="brain-inline-citation"
|
|
471
|
+
aria-label={t(language, "brain.answerProof.marker", { index: index + 1 })}
|
|
472
|
+
aria-controls={citationDomId(messageId, citation.id)}
|
|
473
|
+
title={citation.title}
|
|
474
|
+
onClick={() => focusCitation(citation.id)}
|
|
475
|
+
>
|
|
476
|
+
{index + 1}
|
|
477
|
+
</button>
|
|
478
|
+
))}
|
|
479
|
+
</span>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function AnswerProofCard({ language, proof, messageId }: { language: Language; proof: NonNullable<Message["proof"]>; messageId: string }) {
|
|
484
|
+
return (
|
|
485
|
+
<section className="brain-answer-proof" role="group" aria-label={t(language, "brain.answerProof.aria")}>
|
|
486
|
+
<div className="brain-answer-proof-head">
|
|
487
|
+
<span>{t(language, "brain.answerProof.title")}</span>
|
|
488
|
+
<strong>{proof.provenAcrossModels ? t(language, "brain.answerProof.modelProven", { model: proof.model }) : t(language, "brain.answerProof.modelPending", { model: proof.model })}</strong>
|
|
489
|
+
{proof.citations.length ? (
|
|
490
|
+
<small className="brain-answer-proof-count">{t(language, "brain.answerProof.citationsLabel", { count: proof.citations.length })}</small>
|
|
491
|
+
) : null}
|
|
492
|
+
</div>
|
|
493
|
+
{proof.citations.length ? (
|
|
494
|
+
<ol>
|
|
495
|
+
{proof.citations.map((citation, index) => (
|
|
496
|
+
<li
|
|
497
|
+
key={citation.id}
|
|
498
|
+
id={citationDomId(messageId, citation.id)}
|
|
499
|
+
tabIndex={-1}
|
|
500
|
+
aria-label={t(language, "brain.answerProof.citationItem", { index: index + 1, title: citation.title })}
|
|
501
|
+
>
|
|
502
|
+
<span className="brain-answer-proof-index" aria-hidden="true">{index + 1}</span>
|
|
503
|
+
<span>{citation.source}</span>
|
|
504
|
+
<strong>{citation.title}</strong>
|
|
505
|
+
<small>{citation.snippet || proof.query}</small>
|
|
506
|
+
</li>
|
|
507
|
+
))}
|
|
508
|
+
</ol>
|
|
509
|
+
) : (
|
|
510
|
+
<small>{t(language, "brain.answerProof.empty")}</small>
|
|
511
|
+
)}
|
|
512
|
+
</section>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
141
516
|
function BrainEmptyState({
|
|
142
517
|
language,
|
|
143
518
|
starterPrompts,
|