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.
Files changed (65) hide show
  1. package/README.md +29 -31
  2. package/docs/CHANGELOG.md +48 -0
  3. package/frontend/src/App.tsx +80 -0
  4. package/frontend/src/api/client.ts +11 -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 +382 -7
  14. package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
  15. package/frontend/src/features/brain/BrainHome.tsx +300 -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 +54 -0
  19. package/frontend/src/i18n.ts +374 -0
  20. package/frontend/src/store/appStore.ts +15 -0
  21. package/frontend/src/styles.css +1383 -41
  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/scripts/brain_quality_eval.py +118 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +28 -28
  34. package/static/app/assets/Act-BHEb8bAM.js +2 -0
  35. package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
  36. package/static/app/assets/Brain-yvR7xkrV.js +322 -0
  37. package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
  38. package/static/app/assets/Capture-BrAmsSEH.js +2 -0
  39. package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
  40. package/static/app/assets/Library-BOVzYfxI.js +2 -0
  41. package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
  42. package/static/app/assets/System-D4WaN4kj.js +2 -0
  43. package/static/app/assets/System-D4WaN4kj.js.map +1 -0
  44. package/static/app/assets/index-BOB-W1FZ.js +17 -0
  45. package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
  46. package/static/app/assets/index-DvLsGy-z.css +2 -0
  47. package/static/app/assets/primitives-C3_BfUb8.js +2 -0
  48. package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
  49. package/static/app/assets/textarea-CbvhHvzg.js +2 -0
  50. package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
  51. package/static/app/index.html +2 -2
  52. package/static/app/assets/Act-DGN37eR4.js +0 -2
  53. package/static/app/assets/Brain-HDcIwPHW.js +0 -322
  54. package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
  55. package/static/app/assets/Capture--eZfb_Ex.js +0 -2
  56. package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
  57. package/static/app/assets/Library-BbagRFyd.js +0 -2
  58. package/static/app/assets/System-C6pd3Mp_.js +0 -2
  59. package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
  60. package/static/app/assets/index-C950aES_.js +0 -17
  61. package/static/app/assets/index-C950aES_.js.map +0 -1
  62. package/static/app/assets/index-ztppMg0c.css +0 -2
  63. package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
  64. package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
  65. 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 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,
@@ -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
- <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
110
- <div className="brain-message-bubble">{message.content}</div>
111
- </div>
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,