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.
Files changed (61) hide show
  1. package/README.md +20 -20
  2. package/docs/CHANGELOG.md +23 -0
  3. package/frontend/src/App.tsx +80 -0
  4. package/frontend/src/api/client.ts +6 -0
  5. package/frontend/src/components/AdminAccessGate.tsx +70 -0
  6. package/frontend/src/components/BrainConversation.tsx +232 -8
  7. package/frontend/src/components/FeedbackState.tsx +45 -0
  8. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
  9. package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
  10. package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
  11. package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
  12. package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
  13. package/frontend/src/features/brain/BrainConversation.tsx +232 -19
  14. package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
  15. package/frontend/src/features/brain/BrainHome.tsx +193 -1
  16. package/frontend/src/features/brain/brainData.ts +25 -1
  17. package/frontend/src/features/brain/graphLayout.ts +12 -1
  18. package/frontend/src/features/brain/types.ts +41 -0
  19. package/frontend/src/i18n.ts +304 -0
  20. package/frontend/src/store/appStore.ts +15 -0
  21. package/frontend/src/styles.css +1130 -0
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/workspace.py +59 -0
  26. package/latticeai/core/marketplace.py +1 -1
  27. package/latticeai/core/workspace_os.py +1 -1
  28. package/package.json +1 -1
  29. package/src-tauri/Cargo.lock +1 -1
  30. package/src-tauri/Cargo.toml +1 -1
  31. package/src-tauri/tauri.conf.json +1 -1
  32. package/static/app/asset-manifest.json +28 -28
  33. package/static/app/assets/Act-BHEb8bAM.js +2 -0
  34. package/static/app/assets/{Act-DhLaHnrT.js.map → Act-BHEb8bAM.js.map} +1 -1
  35. package/static/app/assets/Brain-yvR7xkrV.js +322 -0
  36. package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
  37. package/static/app/assets/{Capture-CAKouj52.js → Capture-BrAmsSEH.js} +2 -2
  38. package/static/app/assets/{Capture-CAKouj52.js.map → Capture-BrAmsSEH.js.map} +1 -1
  39. package/static/app/assets/Library-BOVzYfxI.js +2 -0
  40. package/static/app/assets/{Library-B_hW4AwR.js.map → Library-BOVzYfxI.js.map} +1 -1
  41. package/static/app/assets/System-D4WaN4kj.js +2 -0
  42. package/static/app/assets/System-D4WaN4kj.js.map +1 -0
  43. package/static/app/assets/index-BOB-W1FZ.js +17 -0
  44. package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
  45. package/static/app/assets/{index-DKFpn5Kn.css → index-DvLsGy-z.css} +1 -1
  46. package/static/app/assets/primitives-C3_BfUb8.js +2 -0
  47. package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
  48. package/static/app/assets/textarea-CbvhHvzg.js +2 -0
  49. package/static/app/assets/{textarea-DR2Tyy_6.js.map → textarea-CbvhHvzg.js.map} +1 -1
  50. package/static/app/index.html +2 -2
  51. package/static/app/assets/Act-DhLaHnrT.js +0 -2
  52. package/static/app/assets/Brain-DS7NULyY.js +0 -322
  53. package/static/app/assets/Brain-DS7NULyY.js.map +0 -1
  54. package/static/app/assets/Library-B_hW4AwR.js +0 -2
  55. package/static/app/assets/System-9OAoXWPz.js +0 -2
  56. package/static/app/assets/System-9OAoXWPz.js.map +0 -1
  57. package/static/app/assets/index-C-7aHabu.js +0 -17
  58. package/static/app/assets/index-C-7aHabu.js.map +0 -1
  59. package/static/app/assets/primitives-DcO2H3yh.js +0 -2
  60. package/static/app/assets/primitives-DcO2H3yh.js.map +0 -1
  61. 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 type { BrainDepth, BrainProof, BrainReadiness, KnowledgeConcept, MemoryFragment, Message } from "./types";
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
- <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
132
- <div className="brain-message-bubble">{message.content}</div>
133
- {message.role === "assistant" && message.proof ? (
134
- <AnswerProofCard language={language} proof={message.proof} />
135
- ) : null}
136
- </div>
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 className={`brain-ingest-tile is-primary ${uploadingDocument ? "is-disabled" : ""}`}>
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="brain-ingest-tile"
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="brain-ingest-tile"
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="brain-ingest-tile"
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 AnswerProofCard({ language, proof }: { language: Language; proof: NonNullable<Message["proof"]> }) {
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
- <div className="brain-answer-proof" aria-label={t(language, "brain.answerProof.aria")}>
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 key={citation.id}>
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
- </div>
512
+ </section>
300
513
  );
301
514
  }
302
515