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,688 +0,0 @@
1
- import * as React from "react";
2
- import {
3
- BrainCircuit,
4
- CheckCircle2,
5
- Cpu,
6
- DatabaseZap,
7
- FileText,
8
- FileUp,
9
- FolderPlus,
10
- Globe2,
11
- HardDrive,
12
- Loader2,
13
- Repeat2,
14
- Search,
15
- ShieldCheck,
16
- Sparkles,
17
- } from "lucide-react";
18
-
19
- const INGESTION_TYPE_LABEL_KEY: Record<IngestionSourceType, string> = {
20
- file: "brain.ingest.type.file",
21
- folder: "brain.ingest.type.folder",
22
- note: "brain.ingest.type.note",
23
- web: "brain.ingest.type.web",
24
- };
25
- import { LanguageSwitcher } from "@/components/LanguageSwitcher";
26
- import { t, type Language } from "@/i18n";
27
- import {
28
- INGESTION_STAGE_ORDER,
29
- type BrainDepth,
30
- type BrainProof,
31
- type BrainReadiness,
32
- type EmergenceEvent,
33
- type IngestionPipelineStage,
34
- type IngestionSourceType,
35
- type IngestionState,
36
- type KnowledgeConcept,
37
- type MemoryFragment,
38
- type Message,
39
- } from "./types";
40
- import { BrainCarePanel } from "./BrainCarePanel";
41
- import { BrainComposer } from "./BrainComposer";
42
- import { BrainOverviewPanel } from "./BrainOverviewPanel";
43
-
44
- export function BrainConversation({
45
- language,
46
- modelName,
47
- messages,
48
- starterPrompts,
49
- memoryFeedback,
50
- ingestionStates,
51
- emergenceEvents,
52
- draft,
53
- streaming,
54
- imageData,
55
- streamRef,
56
- memories,
57
- concepts,
58
- readiness,
59
- proof,
60
- uploadingDocument,
61
- onOpenDepth,
62
- onDraftChange,
63
- onImageDataChange,
64
- onUploadDocument,
65
- onConnectFolder,
66
- onIngestNote,
67
- onIngestWeb,
68
- onVerifyModelContinuity,
69
- onSend,
70
- }: {
71
- language: Language;
72
- explorationDepth: BrainDepth;
73
- modelName: string;
74
- messages: Message[];
75
- starterPrompts: string[];
76
- memoryFeedback: string | null;
77
- ingestionStates: Record<IngestionSourceType, IngestionState | null>;
78
- emergenceEvents: EmergenceEvent[];
79
- draft: string;
80
- streaming: boolean;
81
- imageData: string | null;
82
- streamRef: React.RefObject<HTMLDivElement | null>;
83
- memories: MemoryFragment[];
84
- concepts: KnowledgeConcept[];
85
- readiness: BrainReadiness;
86
- proof: BrainProof;
87
- uploadingDocument: boolean;
88
- onOpenDepth: (depth: BrainDepth) => void;
89
- onDraftChange: (value: string) => void;
90
- onImageDataChange: (value: string | null) => void;
91
- onUploadDocument: (file: File) => void;
92
- onConnectFolder: (path: string) => void;
93
- onIngestNote: (note: string) => void;
94
- onIngestWeb: (url: string) => void;
95
- onVerifyModelContinuity: () => void;
96
- onSend: () => void;
97
- }) {
98
- return (
99
- <section className="brain-conversation" aria-label={t(language, "brain.aria.conversation")}>
100
- <div className="brain-conversation-header">
101
- <div className="brain-conversation-title">
102
- <h1>
103
- {t(language, "brain.title")}
104
- <span className="brain-edition-badge" title={t(language, "brain.edition.tip")}>
105
- {t(language, "brain.edition")}
106
- </span>
107
- </h1>
108
- <span>{t(language, "brain.chatHome.headerLine")}</span>
109
- </div>
110
- <div className="brain-header-tools">
111
- <LanguageSwitcher compact />
112
- <div className="brain-model-pill">{modelName}</div>
113
- <button className="brain-admin-link" type="button" onClick={() => navigateHash("/admin")}>
114
- <ShieldCheck className="h-3.5 w-3.5" />
115
- {t(language, "brain.admin")}
116
- </button>
117
- </div>
118
- </div>
119
-
120
- <div className="brain-chat-home-layout">
121
- <section className="brain-chat-home-card" aria-label={t(language, "brain.chatHome.aria")}>
122
- <div className="brain-chat-home-head">
123
- <div>
124
- <span>{t(language, "brain.chatHome.kicker")}</span>
125
- <h2>{t(language, "brain.chatHome.title")}</h2>
126
- <p>{t(language, "brain.chatHome.body")}</p>
127
- </div>
128
- <div className="brain-chat-home-proof" aria-label={t(language, "brain.aria.ownership")}>
129
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.local")}</span>
130
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.private")}</span>
131
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.portable")}</span>
132
- </div>
133
- </div>
134
-
135
- <div ref={streamRef} className="brain-stream">
136
- {messages.length === 0 ? (
137
- <BrainEmptyState
138
- language={language}
139
- starterPrompts={starterPrompts}
140
- uploadingDocument={uploadingDocument}
141
- onDraftChange={onDraftChange}
142
- onUploadDocument={onUploadDocument}
143
- />
144
- ) : (
145
- messages.map((message, index) => {
146
- const messageId = `brain-msg-${index}`;
147
- const proof = message.role === "assistant" ? message.proof : undefined;
148
- return (
149
- <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
150
- <div className="brain-message-bubble">
151
- {message.content}
152
- {proof && proof.citations.length ? (
153
- <InlineCitationMarkers language={language} proof={proof} messageId={messageId} />
154
- ) : null}
155
- </div>
156
- {proof ? <AnswerProofCard language={language} proof={proof} messageId={messageId} /> : null}
157
- </div>
158
- );
159
- })
160
- )}
161
- </div>
162
-
163
- {memoryFeedback ? (
164
- <div className="brain-save-feedback" role="status">
165
- <DatabaseZap className="h-3.5 w-3.5" />
166
- <span>{memoryFeedback}</span>
167
- <small>{t(language, "brain.saved.detail")}</small>
168
- </div>
169
- ) : null}
170
-
171
- <BrainComposer
172
- language={language}
173
- draft={draft}
174
- streaming={streaming}
175
- imageData={imageData}
176
- uploadingDocument={uploadingDocument}
177
- onDraftChange={onDraftChange}
178
- onImageDataChange={onImageDataChange}
179
- onUploadDocument={onUploadDocument}
180
- onSend={onSend}
181
- />
182
-
183
- <details className="brain-utility-drawer">
184
- <summary>{t(language, "brain.chatHome.utility")}</summary>
185
- <div className="brain-utility-grid">
186
- <BrainIngestionPanel
187
- language={language}
188
- uploadingDocument={uploadingDocument}
189
- ingestionStates={ingestionStates}
190
- onUploadDocument={onUploadDocument}
191
- onConnectFolder={onConnectFolder}
192
- onIngestNote={onIngestNote}
193
- onIngestWeb={onIngestWeb}
194
- />
195
- <ProductCommandCenter
196
- language={language}
197
- readiness={readiness}
198
- proof={proof}
199
- modelName={modelName}
200
- memories={memories}
201
- concepts={concepts}
202
- emergenceEvents={emergenceEvents}
203
- onOpenDepth={onOpenDepth}
204
- onVerifyModelContinuity={onVerifyModelContinuity}
205
- />
206
- <IngestionTimelineSection language={language} emergenceEvents={emergenceEvents} />
207
- <ModelContinuityDemo
208
- language={language}
209
- proof={proof}
210
- modelName={modelName}
211
- onVerify={onVerifyModelContinuity}
212
- />
213
- <BrainOverviewPanel
214
- memories={memories}
215
- concepts={concepts}
216
- readiness={readiness}
217
- proof={proof}
218
- onOpenDepth={onOpenDepth}
219
- />
220
- <BrainCarePanel language={language} />
221
- </div>
222
- </details>
223
- </section>
224
- </div>
225
- </section>
226
- );
227
- }
228
-
229
- function ProductCommandCenter({
230
- language,
231
- readiness,
232
- proof,
233
- modelName,
234
- memories,
235
- concepts,
236
- emergenceEvents,
237
- onOpenDepth,
238
- onVerifyModelContinuity,
239
- }: {
240
- language: Language;
241
- readiness: BrainReadiness;
242
- proof: BrainProof;
243
- modelName: string;
244
- memories: MemoryFragment[];
245
- concepts: KnowledgeConcept[];
246
- emergenceEvents: EmergenceEvent[];
247
- onOpenDepth: (depth: BrainDepth) => void;
248
- onVerifyModelContinuity: () => void;
249
- }) {
250
- const score = Math.max(0, Math.min(100, readiness.score));
251
- const nextKey =
252
- readiness.state === "alive"
253
- ? "brain.command.next.alive"
254
- : readiness.state === "forming"
255
- ? "brain.command.next.forming"
256
- : "brain.command.next.empty";
257
- const recallable = (proof.recall && proof.recall.items) ? proof.recall.items.length : (proof.proofs ? (proof.proofs.durableItems || 1) : 1);
258
- const latestSource = emergenceEvents[0]?.label ?? t(language, "brain.command.source.empty");
259
-
260
- return (
261
- <section className="brain-command-center" aria-label={t(language, "brain.command.aria")}>
262
- <div className="brain-command-head">
263
- <div>
264
- <span>{t(language, "brain.command.kicker")}</span>
265
- <strong>{t(language, "brain.command.title")}</strong>
266
- </div>
267
- <div className="brain-command-score" role="meter" aria-valuemin={0} aria-valuemax={100} aria-valuenow={score}>
268
- <span>{t(language, "brain.command.score")}</span>
269
- <strong>{score}%</strong>
270
- </div>
271
- </div>
272
-
273
- <div className="brain-command-next">
274
- <BrainCircuit className="h-4 w-4" />
275
- <span>{t(language, "brain.command.next")}</span>
276
- <strong>{t(language, nextKey)}</strong>
277
- </div>
278
-
279
- <div className="brain-command-metrics" aria-label={t(language, "brain.command.metrics")}>
280
- <span>{t(language, "brain.command.metric.memories", { count: memories.length })}</span>
281
- <span>{t(language, "brain.command.metric.topics", { count: concepts.length })}</span>
282
- <span>{t(language, "brain.command.metric.sources", { count: readiness.signals.healthySources })}</span>
283
- <span>{t(language, "brain.command.metric.proof", { count: recallable })}</span>
284
- </div>
285
-
286
- <div className="brain-command-actions">
287
- <button type="button" aria-label={t(language, "brain.command.action.add")} onClick={() => navigateHash("/capture")}>
288
- <FileUp className="h-4 w-4" />
289
- <span>{t(language, "brain.command.action.add")}</span>
290
- <small>{latestSource}</small>
291
- </button>
292
- <button type="button" aria-label={t(language, "brain.command.action.find")} onClick={() => onOpenDepth(3)}>
293
- <Search className="h-4 w-4" />
294
- <span>{t(language, "brain.command.action.find")}</span>
295
- <small>{t(language, "brain.command.action.find.detail")}</small>
296
- </button>
297
- <button type="button" aria-label={t(language, "brain.command.action.proof")} onClick={onVerifyModelContinuity}>
298
- <Repeat2 className="h-4 w-4" />
299
- <span>{t(language, "brain.command.action.proof")}</span>
300
- <small>{proof.modelContinuity.proven ? proof.modelContinuity.activeModel || modelName : modelName}</small>
301
- </button>
302
- <button type="button" aria-label={t(language, "brain.command.action.own")} onClick={() => navigateHash("/settings")}>
303
- <HardDrive className="h-4 w-4" />
304
- <span>{t(language, "brain.command.action.own")}</span>
305
- <small>{t(language, "brain.command.action.own.detail")}</small>
306
- </button>
307
- </div>
308
-
309
- <div className="brain-command-signals">
310
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.command.signal.local")}</span>
311
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.command.signal.private")}</span>
312
- <span><CheckCircle2 className="h-3.5 w-3.5" />{t(language, "brain.command.signal.portable")}</span>
313
- </div>
314
- </section>
315
- );
316
- }
317
-
318
- function BrainIngestionPanel({
319
- language,
320
- uploadingDocument,
321
- ingestionStates,
322
- onUploadDocument,
323
- onConnectFolder,
324
- onIngestNote,
325
- onIngestWeb,
326
- }: {
327
- language: Language;
328
- uploadingDocument: boolean;
329
- ingestionStates: Record<IngestionSourceType, IngestionState | null>;
330
- onUploadDocument: (file: File) => void;
331
- onConnectFolder: (path: string) => void;
332
- onIngestNote: (note: string) => void;
333
- onIngestWeb: (url: string) => void;
334
- }) {
335
- const [folderPath, setFolderPath] = React.useState("");
336
- const [note, setNote] = React.useState("");
337
- const [url, setUrl] = React.useState("");
338
-
339
- return (
340
- <section className="brain-ingestion-panel" aria-label={t(language, "brain.ingest.aria")}>
341
- <div className="brain-ingestion-head">
342
- <span>{t(language, "brain.ingest.kicker")}</span>
343
- <strong>{t(language, "brain.ingest.title")}</strong>
344
- </div>
345
- <div className="brain-ingestion-grid">
346
- <label
347
- className={`brain-ingest-tile is-primary ${tileStateClass(ingestionStates.file)} ${uploadingDocument ? "is-disabled" : ""}`}
348
- >
349
- <FileUp className="h-4 w-4" />
350
- <span>{uploadingDocument ? t(language, "brain.upload.uploading") : t(language, "brain.ingest.file")}</span>
351
- <small>{t(language, "brain.ingest.file.detail")}</small>
352
- <input
353
- type="file"
354
- accept=".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,application/pdf,text/plain,text/markdown,text/csv"
355
- className="sr-only"
356
- disabled={uploadingDocument}
357
- onChange={(event) => {
358
- const file = event.target.files?.[0];
359
- event.currentTarget.value = "";
360
- if (file) onUploadDocument(file);
361
- }}
362
- />
363
- <IngestionStageTrack language={language} state={ingestionStates.file} ctaKey="brain.ingest.cta.file" />
364
- </label>
365
- <form
366
- className={`brain-ingest-tile ${tileStateClass(ingestionStates.folder)}`}
367
- onSubmit={(event) => {
368
- event.preventDefault();
369
- onConnectFolder(folderPath);
370
- setFolderPath("");
371
- }}
372
- >
373
- <FolderPlus className="h-4 w-4" />
374
- <span>{t(language, "brain.ingest.folder")}</span>
375
- <input value={folderPath} onChange={(event) => setFolderPath(event.target.value)} placeholder={t(language, "brain.ingest.folder.placeholder")} />
376
- <IngestionStageTrack language={language} state={ingestionStates.folder} ctaKey="brain.ingest.cta.folder" />
377
- </form>
378
- <form
379
- className={`brain-ingest-tile ${tileStateClass(ingestionStates.note)}`}
380
- onSubmit={(event) => {
381
- event.preventDefault();
382
- onIngestNote(note);
383
- setNote("");
384
- }}
385
- >
386
- <FileText className="h-4 w-4" />
387
- <span>{t(language, "brain.ingest.note")}</span>
388
- <input value={note} onChange={(event) => setNote(event.target.value)} placeholder={t(language, "brain.ingest.note.placeholder")} />
389
- <IngestionStageTrack language={language} state={ingestionStates.note} ctaKey="brain.ingest.cta.note" />
390
- </form>
391
- <form
392
- className={`brain-ingest-tile ${tileStateClass(ingestionStates.web)}`}
393
- onSubmit={(event) => {
394
- event.preventDefault();
395
- onIngestWeb(url);
396
- setUrl("");
397
- }}
398
- >
399
- <Globe2 className="h-4 w-4" />
400
- <span>{t(language, "brain.ingest.web")}</span>
401
- <input value={url} onChange={(event) => setUrl(event.target.value)} placeholder={t(language, "brain.ingest.web.placeholder")} />
402
- <IngestionStageTrack language={language} state={ingestionStates.web} ctaKey="brain.ingest.cta.web" />
403
- </form>
404
- </div>
405
- </section>
406
- );
407
- }
408
-
409
- function tileStateClass(state: IngestionState | null): string {
410
- if (!state) return "";
411
- if (state.stage === "error") return "is-failed";
412
- if (state.stage === "complete") return "is-emerged";
413
- return "is-ingesting";
414
- }
415
-
416
- const STAGE_HINT_KEY: Record<IngestionPipelineStage, string> = {
417
- preparing: "brain.ingest.stage.preparing.hint",
418
- parsing: "brain.ingest.stage.parsing.hint",
419
- embedding: "brain.ingest.stage.embedding.hint",
420
- indexing: "brain.ingest.stage.indexing.hint",
421
- complete: "brain.ingest.stage.complete.hint",
422
- error: "brain.ingest.stage.error",
423
- };
424
-
425
- // Progressive disclosure of the pipeline: collect -> parse -> embed -> memorize.
426
- function IngestionStageTrack({
427
- language,
428
- state,
429
- ctaKey,
430
- }: {
431
- language: Language;
432
- state: IngestionState | null;
433
- ctaKey: string;
434
- }) {
435
- if (!state) {
436
- return <small className="brain-ingest-cta">{t(language, ctaKey)}</small>;
437
- }
438
-
439
- const activeIndex = INGESTION_STAGE_ORDER.indexOf(state.stage);
440
- const isError = state.stage === "error";
441
- const isComplete = state.stage === "complete";
442
- const hasEmergence = state.newMemories > 0 || state.newEntities > 0;
443
-
444
- return (
445
- <div
446
- className="brain-ingest-progress"
447
- role="status"
448
- aria-live="polite"
449
- aria-label={t(language, "brain.ingest.progress.aria", { label: state.label })}
450
- >
451
- <div className="brain-ingest-stage-badges" aria-hidden="true">
452
- {INGESTION_STAGE_ORDER.filter((stage) => stage !== "complete").map((stage) => {
453
- const index = INGESTION_STAGE_ORDER.indexOf(stage);
454
- const done = !isError && !isComplete && index < activeIndex;
455
- const active = !isError && !isComplete && index === activeIndex;
456
- const passed = isComplete;
457
- const cls = isError
458
- ? "is-failed"
459
- : passed || done
460
- ? "is-done"
461
- : active
462
- ? "is-active"
463
- : "";
464
- return (
465
- <span key={stage} className={`brain-ingest-stage-badge ${cls}`}>
466
- {(active || (isError && index === activeIndex)) ? <Loader2 className="h-3 w-3 brain-ingest-spin" /> : null}
467
- {t(language, `brain.ingest.stage.${stage}`)}
468
- </span>
469
- );
470
- })}
471
- </div>
472
- <small className={`brain-ingest-stage-hint ${isError ? "is-failed" : isComplete ? "is-emerged" : ""}`}>
473
- {isError
474
- ? t(language, "brain.ingest.stage.error")
475
- : isComplete
476
- ? hasEmergence
477
- ? t(language, "brain.ingest.result", { memories: state.newMemories, entities: state.newEntities })
478
- : t(language, "brain.ingest.result.empty")
479
- : t(language, STAGE_HINT_KEY[state.stage])}
480
- </small>
481
- </div>
482
- );
483
- }
484
-
485
- function relativeTime(language: Language, at: number, now: number): string {
486
- const diffMinutes = Math.floor((now - at) / 60000);
487
- if (diffMinutes < 1) return t(language, "brain.timeline.justNow");
488
- return t(language, "brain.timeline.minutesAgo", { count: diffMinutes });
489
- }
490
-
491
- function IngestionTimelineSection({
492
- language,
493
- emergenceEvents,
494
- }: {
495
- language: Language;
496
- emergenceEvents: EmergenceEvent[];
497
- }) {
498
- // Re-read the clock on each render of the parent so relative labels stay fresh-enough.
499
- const now = Date.now();
500
- return (
501
- <section
502
- className="brain-emergence-timeline"
503
- aria-label={t(language, "brain.timeline.aria")}
504
- aria-live="polite"
505
- >
506
- <div className="brain-emergence-head">
507
- <Sparkles className="h-3.5 w-3.5" />
508
- <strong>{t(language, "brain.timeline.emergenceTitle")}</strong>
509
- <span>{t(language, "brain.timeline.recent")}</span>
510
- </div>
511
- {emergenceEvents.length === 0 ? (
512
- <p className="brain-emergence-empty">{t(language, "brain.timeline.empty")}</p>
513
- ) : (
514
- <ol className="brain-emergence-list">
515
- {emergenceEvents.map((event) => (
516
- <li key={event.id} className="brain-emergence-item">
517
- <span className="brain-emergence-type">{t(language, INGESTION_TYPE_LABEL_KEY[event.sourceType])}</span>
518
- <span className="brain-emergence-label">{event.label}</span>
519
- <span className="brain-emergence-counts">
520
- <strong>{t(language, "brain.timeline.newMemories", { count: event.newMemories })}</strong>
521
- <strong>{t(language, "brain.timeline.newEntities", { count: event.newEntities })}</strong>
522
- </span>
523
- <time className="brain-emergence-at">{relativeTime(language, event.at, now)}</time>
524
- </li>
525
- ))}
526
- </ol>
527
- )}
528
- </section>
529
- );
530
- }
531
-
532
- function ModelContinuityDemo({
533
- language,
534
- proof,
535
- modelName,
536
- onVerify,
537
- }: {
538
- language: Language;
539
- proof: BrainProof;
540
- modelName: string;
541
- onVerify: () => void;
542
- }) {
543
- return (
544
- <section className="brain-model-demo" aria-label={t(language, "brain.modelDemo.aria")}>
545
- <div>
546
- <span>{t(language, "brain.modelDemo.kicker")}</span>
547
- <strong>{proof.modelContinuity.proven ? t(language, "brain.modelDemo.proven") : t(language, "brain.modelDemo.pending")}</strong>
548
- <small>{t(language, "brain.modelDemo.detail", { model: proof.modelContinuity.activeModel || modelName })}</small>
549
- </div>
550
- <button type="button" onClick={onVerify}>
551
- <Repeat2 className="h-3.5 w-3.5" />
552
- {t(language, "brain.modelDemo.verify")}
553
- </button>
554
- <button type="button" onClick={() => navigateHash("/models")}>
555
- <Cpu className="h-3.5 w-3.5" />
556
- {t(language, "brain.modelDemo.change")}
557
- </button>
558
- </section>
559
- );
560
- }
561
-
562
- function citationDomId(messageId: string, citationId: string): string {
563
- return `${messageId}-cite-${citationId}`;
564
- }
565
-
566
- // Inline, keyboard-focusable citation markers rendered next to the answer text.
567
- // Activating one moves focus to the matching row in the evidence card below.
568
- function InlineCitationMarkers({
569
- language,
570
- proof,
571
- messageId,
572
- }: {
573
- language: Language;
574
- proof: NonNullable<Message["proof"]>;
575
- messageId: string;
576
- }) {
577
- const focusCitation = (citationId: string) => {
578
- const target = document.getElementById(citationDomId(messageId, citationId));
579
- if (target) {
580
- target.scrollIntoView({ behavior: "smooth", block: "nearest" });
581
- target.focus();
582
- }
583
- };
584
- return (
585
- <span className="brain-inline-citations" aria-label={t(language, "brain.answerProof.citationsLabel", { count: proof.citations.length })}>
586
- {proof.citations.map((citation, index) => (
587
- <button
588
- key={citation.id}
589
- type="button"
590
- className="brain-inline-citation"
591
- aria-label={t(language, "brain.answerProof.marker", { index: index + 1 })}
592
- aria-controls={citationDomId(messageId, citation.id)}
593
- title={citation.title}
594
- onClick={() => focusCitation(citation.id)}
595
- >
596
- {index + 1}
597
- </button>
598
- ))}
599
- </span>
600
- );
601
- }
602
-
603
- function AnswerProofCard({ language, proof, messageId }: { language: Language; proof: NonNullable<Message["proof"]>; messageId: string }) {
604
- return (
605
- <section className="brain-answer-proof" role="group" aria-label={t(language, "brain.answerProof.aria")}>
606
- <div className="brain-answer-proof-head">
607
- <span>{t(language, "brain.answerProof.title")}</span>
608
- <strong>{proof.provenAcrossModels ? t(language, "brain.answerProof.modelProven", { model: proof.model }) : t(language, "brain.answerProof.modelPending", { model: proof.model })}</strong>
609
- {proof.citations.length ? (
610
- <small className="brain-answer-proof-count">{t(language, "brain.answerProof.citationsLabel", { count: proof.citations.length })}</small>
611
- ) : null}
612
- </div>
613
- {proof.citations.length ? (
614
- <ol>
615
- {proof.citations.map((citation, index) => (
616
- <li
617
- key={citation.id}
618
- id={citationDomId(messageId, citation.id)}
619
- tabIndex={-1}
620
- aria-label={t(language, "brain.answerProof.citationItem", { index: index + 1, title: citation.title })}
621
- >
622
- <span className="brain-answer-proof-index" aria-hidden="true">{index + 1}</span>
623
- <span>{citation.source}</span>
624
- <strong>{citation.title}</strong>
625
- <small>{citation.snippet || proof.query}</small>
626
- </li>
627
- ))}
628
- </ol>
629
- ) : (
630
- <small>{t(language, "brain.answerProof.empty")}</small>
631
- )}
632
- </section>
633
- );
634
- }
635
-
636
- function BrainEmptyState({
637
- language,
638
- starterPrompts,
639
- uploadingDocument,
640
- onDraftChange,
641
- onUploadDocument,
642
- }: {
643
- language: Language;
644
- starterPrompts: string[];
645
- uploadingDocument: boolean;
646
- onDraftChange: (value: string) => void;
647
- onUploadDocument: (file: File) => void;
648
- }) {
649
- return (
650
- <div className="mind-empty">
651
- <div className="mind-empty-kicker">{t(language, "brain.empty.kicker")}</div>
652
- <div className="mind-empty-title">{t(language, "brain.empty.title")}</div>
653
- <p>{t(language, "brain.empty.body")}</p>
654
- <label className={`mind-empty-upload ${uploadingDocument ? "is-disabled" : ""}`}>
655
- <DatabaseZap className="h-3.5 w-3.5" />
656
- <span>{uploadingDocument ? t(language, "brain.upload.uploading") : t(language, "brain.upload.cta")}</span>
657
- <input
658
- type="file"
659
- accept=".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,application/pdf,text/plain,text/markdown,text/csv"
660
- className="sr-only"
661
- disabled={uploadingDocument}
662
- onChange={(event) => {
663
- const file = event.target.files?.[0];
664
- event.currentTarget.value = "";
665
- if (file) onUploadDocument(file);
666
- }}
667
- />
668
- </label>
669
- <small className="mind-empty-upload-hint">{t(language, "brain.upload.hint")}</small>
670
- <div className="mind-empty-prompts" aria-label={t(language, "brain.aria.starterPrompts")}>
671
- {starterPrompts.map((prompt) => (
672
- <button key={prompt} type="button" onClick={() => onDraftChange(prompt)}>
673
- {prompt}
674
- </button>
675
- ))}
676
- </div>
677
- <div className="mind-empty-trail" aria-label={t(language, "brain.empty.trail.label")}>
678
- <span>{t(language, "brain.empty.trail.save")}</span>
679
- <span>{t(language, "brain.empty.trail.recall")}</span>
680
- <span>{t(language, "brain.empty.trail.backup")}</span>
681
- </div>
682
- </div>
683
- );
684
- }
685
-
686
- function navigateHash(route: string) {
687
- window.location.hash = route;
688
- }