ltcai 7.0.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 +20 -20
- package/docs/CHANGELOG.md +23 -0
- package/frontend/src/App.tsx +80 -0
- package/frontend/src/api/client.ts +6 -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 +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 +304 -0
- package/frontend/src/store/appStore.ts +15 -0
- package/frontend/src/styles.css +1130 -0
- 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/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-DhLaHnrT.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-CAKouj52.js → Capture-BrAmsSEH.js} +2 -2
- package/static/app/assets/{Capture-CAKouj52.js.map → Capture-BrAmsSEH.js.map} +1 -1
- package/static/app/assets/Library-BOVzYfxI.js +2 -0
- package/static/app/assets/{Library-B_hW4AwR.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-DKFpn5Kn.css → index-DvLsGy-z.css} +1 -1
- 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-DR2Tyy_6.js.map → textarea-CbvhHvzg.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
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Cpu, DatabaseZap, FileText, FileUp, FolderPlus, Globe2, Repeat2, 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,
|
|
@@ -39,6 +60,8 @@ export function BrainConversation({
|
|
|
39
60
|
messages: Message[];
|
|
40
61
|
starterPrompts: string[];
|
|
41
62
|
memoryFeedback: string | null;
|
|
63
|
+
ingestionStates: Record<IngestionSourceType, IngestionState | null>;
|
|
64
|
+
emergenceEvents: EmergenceEvent[];
|
|
42
65
|
draft: string;
|
|
43
66
|
streaming: boolean;
|
|
44
67
|
imageData: string | null;
|
|
@@ -100,11 +123,13 @@ export function BrainConversation({
|
|
|
100
123
|
<BrainIngestionPanel
|
|
101
124
|
language={language}
|
|
102
125
|
uploadingDocument={uploadingDocument}
|
|
126
|
+
ingestionStates={ingestionStates}
|
|
103
127
|
onUploadDocument={onUploadDocument}
|
|
104
128
|
onConnectFolder={onConnectFolder}
|
|
105
129
|
onIngestNote={onIngestNote}
|
|
106
130
|
onIngestWeb={onIngestWeb}
|
|
107
131
|
/>
|
|
132
|
+
<IngestionTimelineSection language={language} emergenceEvents={emergenceEvents} />
|
|
108
133
|
<BrainOverviewPanel
|
|
109
134
|
memories={memories}
|
|
110
135
|
concepts={concepts}
|
|
@@ -127,14 +152,21 @@ export function BrainConversation({
|
|
|
127
152
|
onUploadDocument={onUploadDocument}
|
|
128
153
|
/>
|
|
129
154
|
) : (
|
|
130
|
-
messages.map((message, index) =>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
})
|
|
138
170
|
)}
|
|
139
171
|
</div>
|
|
140
172
|
|
|
@@ -166,6 +198,7 @@ export function BrainConversation({
|
|
|
166
198
|
function BrainIngestionPanel({
|
|
167
199
|
language,
|
|
168
200
|
uploadingDocument,
|
|
201
|
+
ingestionStates,
|
|
169
202
|
onUploadDocument,
|
|
170
203
|
onConnectFolder,
|
|
171
204
|
onIngestNote,
|
|
@@ -173,6 +206,7 @@ function BrainIngestionPanel({
|
|
|
173
206
|
}: {
|
|
174
207
|
language: Language;
|
|
175
208
|
uploadingDocument: boolean;
|
|
209
|
+
ingestionStates: Record<IngestionSourceType, IngestionState | null>;
|
|
176
210
|
onUploadDocument: (file: File) => void;
|
|
177
211
|
onConnectFolder: (path: string) => void;
|
|
178
212
|
onIngestNote: (note: string) => void;
|
|
@@ -189,7 +223,9 @@ function BrainIngestionPanel({
|
|
|
189
223
|
<strong>{t(language, "brain.ingest.title")}</strong>
|
|
190
224
|
</div>
|
|
191
225
|
<div className="brain-ingestion-grid">
|
|
192
|
-
<label
|
|
226
|
+
<label
|
|
227
|
+
className={`brain-ingest-tile is-primary ${tileStateClass(ingestionStates.file)} ${uploadingDocument ? "is-disabled" : ""}`}
|
|
228
|
+
>
|
|
193
229
|
<FileUp className="h-4 w-4" />
|
|
194
230
|
<span>{uploadingDocument ? t(language, "brain.upload.uploading") : t(language, "brain.ingest.file")}</span>
|
|
195
231
|
<small>{t(language, "brain.ingest.file.detail")}</small>
|
|
@@ -204,9 +240,10 @@ function BrainIngestionPanel({
|
|
|
204
240
|
if (file) onUploadDocument(file);
|
|
205
241
|
}}
|
|
206
242
|
/>
|
|
243
|
+
<IngestionStageTrack language={language} state={ingestionStates.file} ctaKey="brain.ingest.cta.file" />
|
|
207
244
|
</label>
|
|
208
245
|
<form
|
|
209
|
-
className=
|
|
246
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.folder)}`}
|
|
210
247
|
onSubmit={(event) => {
|
|
211
248
|
event.preventDefault();
|
|
212
249
|
onConnectFolder(folderPath);
|
|
@@ -216,9 +253,10 @@ function BrainIngestionPanel({
|
|
|
216
253
|
<FolderPlus className="h-4 w-4" />
|
|
217
254
|
<span>{t(language, "brain.ingest.folder")}</span>
|
|
218
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" />
|
|
219
257
|
</form>
|
|
220
258
|
<form
|
|
221
|
-
className=
|
|
259
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.note)}`}
|
|
222
260
|
onSubmit={(event) => {
|
|
223
261
|
event.preventDefault();
|
|
224
262
|
onIngestNote(note);
|
|
@@ -228,9 +266,10 @@ function BrainIngestionPanel({
|
|
|
228
266
|
<FileText className="h-4 w-4" />
|
|
229
267
|
<span>{t(language, "brain.ingest.note")}</span>
|
|
230
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" />
|
|
231
270
|
</form>
|
|
232
271
|
<form
|
|
233
|
-
className=
|
|
272
|
+
className={`brain-ingest-tile ${tileStateClass(ingestionStates.web)}`}
|
|
234
273
|
onSubmit={(event) => {
|
|
235
274
|
event.preventDefault();
|
|
236
275
|
onIngestWeb(url);
|
|
@@ -240,12 +279,136 @@ function BrainIngestionPanel({
|
|
|
240
279
|
<Globe2 className="h-4 w-4" />
|
|
241
280
|
<span>{t(language, "brain.ingest.web")}</span>
|
|
242
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" />
|
|
243
283
|
</form>
|
|
244
284
|
</div>
|
|
245
285
|
</section>
|
|
246
286
|
);
|
|
247
287
|
}
|
|
248
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
|
+
|
|
249
412
|
function ModelContinuityDemo({
|
|
250
413
|
language,
|
|
251
414
|
proof,
|
|
@@ -276,17 +439,67 @@ function ModelContinuityDemo({
|
|
|
276
439
|
);
|
|
277
440
|
}
|
|
278
441
|
|
|
279
|
-
function
|
|
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 }) {
|
|
280
484
|
return (
|
|
281
|
-
<
|
|
485
|
+
<section className="brain-answer-proof" role="group" aria-label={t(language, "brain.answerProof.aria")}>
|
|
282
486
|
<div className="brain-answer-proof-head">
|
|
283
487
|
<span>{t(language, "brain.answerProof.title")}</span>
|
|
284
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}
|
|
285
492
|
</div>
|
|
286
493
|
{proof.citations.length ? (
|
|
287
494
|
<ol>
|
|
288
|
-
{proof.citations.map((citation) => (
|
|
289
|
-
<li
|
|
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>
|
|
290
503
|
<span>{citation.source}</span>
|
|
291
504
|
<strong>{citation.title}</strong>
|
|
292
505
|
<small>{citation.snippet || proof.query}</small>
|
|
@@ -296,7 +509,7 @@ function AnswerProofCard({ language, proof }: { language: Language; proof: NonNu
|
|
|
296
509
|
) : (
|
|
297
510
|
<small>{t(language, "brain.answerProof.empty")}</small>
|
|
298
511
|
)}
|
|
299
|
-
</
|
|
512
|
+
</section>
|
|
300
513
|
);
|
|
301
514
|
}
|
|
302
515
|
|