ltcai 6.2.0 → 6.3.1

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 (51) hide show
  1. package/README.md +29 -17
  2. package/docs/CHANGELOG.md +66 -0
  3. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  4. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  5. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  6. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  7. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  8. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -688
  9. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  10. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  11. package/frontend/src/features/admin/AdminConsole.tsx +1 -1
  12. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  13. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  14. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  15. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  16. package/frontend/src/features/brain/BrainHome.tsx +28 -795
  17. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  18. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  19. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  20. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  21. package/frontend/src/features/brain/types.ts +6 -6
  22. package/frontend/src/features/review/ReviewCard.tsx +14 -10
  23. package/frontend/src/features/review/ReviewInbox.tsx +24 -9
  24. package/frontend/src/i18n.ts +208 -0
  25. package/frontend/src/pages/Brain.tsx +63 -5
  26. package/frontend/src/pages/Capture.tsx +139 -43
  27. package/frontend/src/pages/Library.tsx +72 -6
  28. package/lattice_brain/__init__.py +1 -1
  29. package/lattice_brain/runtime/multi_agent.py +1 -1
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/app_factory.py +48 -106
  32. package/latticeai/core/marketplace.py +1 -1
  33. package/latticeai/core/workspace_os.py +1 -1
  34. package/latticeai/runtime/access_runtime.py +97 -0
  35. package/latticeai/runtime/chat_wiring.py +119 -0
  36. package/latticeai/runtime/model_wiring.py +40 -0
  37. package/latticeai/runtime/platform_runtime_wiring.py +2 -3
  38. package/latticeai/runtime/review_wiring.py +37 -0
  39. package/latticeai/runtime/tail_wiring.py +21 -0
  40. package/package.json +2 -2
  41. package/scripts/check_i18n_literals.mjs +52 -0
  42. package/src-tauri/Cargo.lock +1 -1
  43. package/src-tauri/Cargo.toml +1 -1
  44. package/src-tauri/tauri.conf.json +1 -1
  45. package/static/app/asset-manifest.json +5 -5
  46. package/static/app/assets/index-Div5vMlq.css +2 -0
  47. package/static/app/assets/{index-D91Rz5--.js → index-t1jx1BR9.js} +2 -2
  48. package/static/app/assets/index-t1jx1BR9.js.map +1 -0
  49. package/static/app/index.html +2 -2
  50. package/static/app/assets/index-B2-1Gm0q.css +0 -2
  51. package/static/app/assets/index-D91Rz5--.js.map +0 -1
@@ -1,60 +1,13 @@
1
1
  import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import { Archive, ChevronDown, DatabaseBackup, Download, Eye, ImagePlus, ListFilter, RotateCcw, Search, Send, ShieldCheck } from "lucide-react";
4
- import { latticeApi, type ApiResult } from "@/api/client";
5
- import { Button } from "@/components/ui/button";
2
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { latticeApi } from "@/api/client";
6
4
  import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
7
- import { LanguageSwitcher } from "@/components/LanguageSwitcher";
8
5
  import { useAppStore } from "@/store/appStore";
9
- import { asArray } from "@/lib/utils";
10
- import { t, type Language } from "@/i18n";
11
-
12
- type ApiRecord = Record<string, unknown>;
13
- type BrainDepth = 1 | 2 | 3 | 4 | 5;
14
-
15
- type Message = {
16
- role: "user" | "assistant";
17
- content: string;
18
- };
19
-
20
- type MemoryFragment = {
21
- id: string;
22
- title: string;
23
- kind: string;
24
- };
25
-
26
- type KnowledgeConcept = {
27
- id: string;
28
- label: string;
29
- type: string;
30
- summary: string;
31
- importance: number;
32
- };
33
-
34
- type RelationshipThread = {
35
- id: string;
36
- source: string;
37
- target: string;
38
- label: string;
39
- weight: number;
40
- };
41
-
42
- type KnowledgeGraphModel = {
43
- nodes: KnowledgeConcept[];
44
- edges: RelationshipThread[];
45
- };
46
-
47
- const DEPTHS: Array<{ level: BrainDepth; label: string; state: BrainState }> = [
48
- { level: 1, label: "Living Brain", state: "idle" },
49
- { level: 2, label: "Memory Layer", state: "recalling" },
50
- { level: 3, label: "Knowledge Layer", state: "synthesizing" },
51
- { level: 4, label: "Relationship Layer", state: "planning" },
52
- { level: 5, label: "Knowledge Graph", state: "synthesizing" },
53
- ];
54
-
55
- function navigateHash(route: string) {
56
- window.location.hash = route;
57
- }
6
+ import { t } from "@/i18n";
7
+ import { BrainConversation } from "./BrainConversation";
8
+ import { buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
9
+ import { DepthEmergence } from "./DepthEmergence";
10
+ import { DEPTHS, type BrainDepth, type MemoryFragment, type Message } from "./types";
58
11
 
59
12
  export function BrainHome({
60
13
  brainState,
@@ -209,8 +162,8 @@ export function BrainHome({
209
162
  }
210
163
 
211
164
  return (
212
- <main className="brain-home" aria-label="Lattice Brain">
213
- <section className="brain-presence" aria-label="Brain exploration">
165
+ <main className="brain-home" aria-label={t(language, "brain.aria.home")}>
166
+ <section className="brain-presence" aria-label={t(language, "brain.aria.exploration")}>
214
167
  <div className="brain-exploration" data-depth={explorationDepth}>
215
168
  <LivingBrain
216
169
  state={brainState}
@@ -226,7 +179,7 @@ export function BrainHome({
226
179
  <strong>{t(language, `brain.depth.${explorationDepth}`)}</strong>
227
180
  </div>
228
181
 
229
- <div className="brain-depth-actions" aria-label="Brain quick views">
182
+ <div className="brain-depth-actions" aria-label={t(language, "brain.aria.quickViews")}>
230
183
  <button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>{t(language, "brain.view.memories")}</button>
231
184
  <button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>{t(language, "brain.view.topics")}</button>
232
185
  <button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>{t(language, "brain.view.relationships")}</button>
@@ -256,744 +209,24 @@ export function BrainHome({
256
209
  </div>
257
210
  </section>
258
211
 
259
- <section className="brain-conversation" aria-label="Conversation">
260
- <div className="brain-conversation-header">
261
- <div>
262
- <h1>{t(language, "brain.title")}</h1>
263
- <span>{t(language, `brain.depth.${explorationDepth}`)}</span>
264
- </div>
265
- <LanguageSwitcher compact />
266
- <div className="brain-ownership-strip" aria-label="Brain ownership guarantees">
267
- <span>{t(language, "brain.local")}</span>
268
- <span>{t(language, "brain.portable")}</span>
269
- <span>{t(language, "brain.private")}</span>
270
- </div>
271
- <div>{modelName}</div>
272
- <button className="brain-admin-link" type="button" onClick={() => navigateHash("/admin")}>
273
- <ShieldCheck className="h-3.5 w-3.5" />
274
- {t(language, "brain.admin")}
275
- </button>
276
- </div>
277
-
278
- <div ref={streamRef} className="brain-stream">
279
- <BrainOverviewPanel
280
- memories={memoryFragments}
281
- concepts={knowledgeConcepts}
282
- onOpenDepth={jumpToDepth}
283
- />
284
- {messages.length === 0 ? (
285
- <div className="mind-empty">
286
- <div className="mind-empty-kicker">{t(language, "brain.empty.kicker")}</div>
287
- <div className="mind-empty-title">{t(language, "brain.empty.title")}</div>
288
- <p>{t(language, "brain.empty.body")}</p>
289
- <div className="mind-empty-prompts" aria-label="Starter prompts">
290
- {starterPrompts.map((prompt) => (
291
- <button key={prompt} type="button" onClick={() => setDraft(prompt)}>
292
- {prompt}
293
- </button>
294
- ))}
295
- </div>
296
- <div className="mind-empty-trail" aria-label={t(language, "brain.empty.trail.label")}>
297
- <span>{t(language, "brain.empty.trail.save")}</span>
298
- <span>{t(language, "brain.empty.trail.recall")}</span>
299
- <span>{t(language, "brain.empty.trail.backup")}</span>
300
- </div>
301
- </div>
302
- ) : (
303
- messages.map((message, index) => (
304
- <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
305
- <div className="brain-message-bubble">{message.content}</div>
306
- </div>
307
- ))
308
- )}
309
- </div>
310
-
311
- {memoryFeedback ? <div className="brain-save-feedback" role="status">{memoryFeedback}</div> : null}
312
-
313
- <BrainCarePanel language={language} />
314
-
315
- <div className="brain-composer">
316
- <textarea
317
- value={draft}
318
- onChange={(event) => setDraft(event.target.value)}
319
- onKeyDown={(event) => {
320
- if (event.key === "Enter" && !event.shiftKey) {
321
- event.preventDefault();
322
- void send();
323
- }
324
- }}
325
- placeholder={t(language, "brain.placeholder")}
326
- />
327
- <div className="brain-composer-actions">
328
- <label className="brain-image-input">
329
- <ImagePlus className="h-3.5 w-3.5" />
330
- <span>{t(language, "brain.image")}</span>
331
- <input
332
- type="file"
333
- accept="image/*"
334
- className="sr-only"
335
- onChange={async (event) => {
336
- const file = event.target.files?.[0];
337
- if (file) setImageData(await fileToDataUrl(file));
338
- }}
339
- />
340
- </label>
341
- {imageData ? <span className="brain-quiet-success">{t(language, "brain.imageAttached")}</span> : null}
342
- <Button onClick={() => void send()} disabled={!draft.trim() || streaming} className="rounded-full px-5">
343
- <Send className="h-4 w-4" /> {t(language, "brain.send")}
344
- </Button>
345
- </div>
346
- </div>
347
- </section>
212
+ <BrainConversation
213
+ language={language}
214
+ explorationDepth={explorationDepth}
215
+ modelName={modelName}
216
+ messages={messages}
217
+ starterPrompts={starterPrompts}
218
+ memoryFeedback={memoryFeedback}
219
+ draft={draft}
220
+ streaming={streaming}
221
+ imageData={imageData}
222
+ streamRef={streamRef}
223
+ memories={memoryFragments}
224
+ concepts={knowledgeConcepts}
225
+ onOpenDepth={jumpToDepth}
226
+ onDraftChange={setDraft}
227
+ onImageDataChange={setImageData}
228
+ onSend={() => void send()}
229
+ />
348
230
  </main>
349
231
  );
350
232
  }
351
-
352
- function BrainCarePanel({ language }: { language: Language }) {
353
- const qc = useQueryClient();
354
- const [expanded, setExpanded] = React.useState(false);
355
- const [archivePath, setArchivePath] = React.useState("");
356
- const [passphrase, setPassphrase] = React.useState("");
357
- const [latestResult, setLatestResult] = React.useState<ApiResult | null>(null);
358
- const portabilityQ = useQuery({ queryKey: ["portability"], queryFn: latticeApi.graphPortability });
359
- const backupHealthQ = useQuery({ queryKey: ["backupHealth"], queryFn: latticeApi.backupHealth });
360
- const rememberResult = React.useCallback((result: ApiResult) => setLatestResult(result), []);
361
-
362
- const exportGraph = useCareMutation(() => latticeApi.graphExport(), undefined, rememberResult);
363
- const backupGraph = useCareMutation(() => latticeApi.graphBackup(), () => {
364
- void qc.invalidateQueries({ queryKey: ["backupHealth"] });
365
- void qc.invalidateQueries({ queryKey: ["portability"] });
366
- }, rememberResult);
367
- const archiveBrain = useCareMutation(
368
- () => latticeApi.brainArchive({ path: archivePath.trim() || null, passphrase }),
369
- () => void qc.invalidateQueries({ queryKey: ["backupHealth"] }),
370
- rememberResult,
371
- );
372
- const inspectArchive = useCareMutation(() => latticeApi.brainArchiveInspect({
373
- path: archivePath.trim(),
374
- passphrase: passphrase || null,
375
- }), undefined, rememberResult);
376
- const restorePreview = useCareMutation(() => latticeApi.brainArchiveRestore({
377
- path: archivePath.trim(),
378
- passphrase,
379
- dry_run: true,
380
- confirm: false,
381
- }), undefined, rememberResult);
382
-
383
- const portableFormat = portabilityLabel(portabilityQ.data?.data);
384
- const backupStatus = backupHealthLabel(backupHealthQ.data?.data);
385
-
386
- return (
387
- <section className={`brain-care-panel ${expanded ? "is-expanded" : "is-collapsed"}`} aria-label={t(language, "care.title")}>
388
- <button
389
- className="brain-care-summary"
390
- type="button"
391
- aria-expanded={expanded}
392
- aria-controls="brain-care-details"
393
- onClick={() => setExpanded((value) => !value)}
394
- >
395
- <span className="brain-care-summary-main">
396
- <span><ShieldCheck className="h-3.5 w-3.5" /> {t(language, "care.title")}</span>
397
- <strong>{t(language, "care.subtitle")}</strong>
398
- </span>
399
- <div className="brain-care-proof" aria-label="Ownership model">
400
- <span>{t(language, "care.private")}</span>
401
- <span>{portableFormat}</span>
402
- <span>{backupStatus}</span>
403
- </div>
404
- <ChevronDown className="brain-care-toggle h-4 w-4" aria-hidden="true" />
405
- </button>
406
-
407
- {expanded ? (
408
- <div id="brain-care-details" className="brain-care-details">
409
- <div className="brain-care-actions">
410
- <CareButton
411
- icon={<Download className="h-3.5 w-3.5" />}
412
- label={t(language, "care.export")}
413
- detail={t(language, "care.export.detail")}
414
- pendingLabel={t(language, "care.working")}
415
- pending={exportGraph.isPending}
416
- onClick={() => exportGraph.mutate()}
417
- />
418
- <CareButton
419
- icon={<DatabaseBackup className="h-3.5 w-3.5" />}
420
- label={t(language, "care.backup")}
421
- detail={t(language, "care.backup.detail")}
422
- pendingLabel={t(language, "care.working")}
423
- pending={backupGraph.isPending}
424
- onClick={() => backupGraph.mutate()}
425
- />
426
- <CareButton
427
- icon={<Archive className="h-3.5 w-3.5" />}
428
- label={t(language, "care.archive")}
429
- detail={t(language, "care.archive.detail")}
430
- pendingLabel={t(language, "care.working")}
431
- pending={archiveBrain.isPending}
432
- disabled={!passphrase.trim()}
433
- onClick={() => archiveBrain.mutate()}
434
- />
435
- </div>
436
-
437
- <div className="brain-care-archive">
438
- <input
439
- value={archivePath}
440
- onChange={(event) => setArchivePath(event.target.value)}
441
- placeholder={t(language, "care.path.placeholder")}
442
- aria-label={t(language, "care.path.label")}
443
- />
444
- <input
445
- type="password"
446
- value={passphrase}
447
- onChange={(event) => setPassphrase(event.target.value)}
448
- placeholder={t(language, "care.passphrase.placeholder")}
449
- aria-label={t(language, "care.passphrase.label")}
450
- />
451
- <div className="brain-care-archive-actions">
452
- <Button
453
- variant="outline"
454
- size="sm"
455
- disabled={!archivePath.trim() || inspectArchive.isPending}
456
- onClick={() => inspectArchive.mutate()}
457
- >
458
- <Eye className="h-3.5 w-3.5" /> {t(language, "care.inspect")}
459
- </Button>
460
- <Button
461
- variant="outline"
462
- size="sm"
463
- disabled={!archivePath.trim() || !passphrase.trim() || restorePreview.isPending}
464
- onClick={() => restorePreview.mutate()}
465
- >
466
- <RotateCcw className="h-3.5 w-3.5" /> {t(language, "care.restorePreview")}
467
- </Button>
468
- </div>
469
- </div>
470
-
471
- {latestResult ? (
472
- <div className={`brain-care-result ${latestResult.ok ? "is-ok" : "is-error"}`} role="status">
473
- {summarizeCareResult(latestResult)}
474
- </div>
475
- ) : (
476
- <p className="brain-care-note">
477
- {t(language, "care.note")}
478
- </p>
479
- )}
480
- </div>
481
- ) : null}
482
- </section>
483
- );
484
- }
485
-
486
- function useCareMutation<T extends ApiResult>(
487
- mutationFn: () => Promise<T>,
488
- onSuccess?: () => void,
489
- onResult?: (result: T) => void,
490
- ) {
491
- return useMutation({
492
- mutationFn,
493
- onSuccess: (result) => {
494
- onResult?.(result);
495
- onSuccess?.();
496
- },
497
- });
498
- }
499
-
500
- function CareButton({
501
- icon,
502
- label,
503
- detail,
504
- pendingLabel,
505
- pending,
506
- disabled,
507
- onClick,
508
- }: {
509
- icon: React.ReactNode;
510
- label: string;
511
- detail: string;
512
- pendingLabel: string;
513
- pending?: boolean;
514
- disabled?: boolean;
515
- onClick: () => void;
516
- }) {
517
- return (
518
- <button className="brain-care-button" type="button" disabled={disabled || pending} onClick={onClick}>
519
- {icon}
520
- <span>
521
- <strong>{pending ? pendingLabel : label}</strong>
522
- <small>{detail}</small>
523
- </span>
524
- </button>
525
- );
526
- }
527
-
528
- function DepthEmergence({
529
- depth,
530
- memories,
531
- concepts,
532
- relationships,
533
- graphModel,
534
- graphSearch,
535
- selectedGraphId,
536
- onGraphSearch,
537
- onSelectGraphNode,
538
- onRecallMemory,
539
- }: {
540
- depth: BrainDepth;
541
- memories: MemoryFragment[];
542
- concepts: KnowledgeConcept[];
543
- relationships: RelationshipThread[];
544
- graphModel: KnowledgeGraphModel;
545
- graphSearch: string;
546
- selectedGraphId: string | null;
547
- onGraphSearch: (value: string) => void;
548
- onSelectGraphNode: (id: string | null) => void;
549
- onRecallMemory: (fragment: MemoryFragment) => void;
550
- }) {
551
- if (depth === 1) return null;
552
-
553
- return (
554
- <>
555
- {depth >= 2 ? (
556
- <MemoryLayer memories={memories} depth={depth} onRecallMemory={onRecallMemory} />
557
- ) : null}
558
- {depth >= 3 && depth < 5 ? (
559
- <KnowledgeLayer concepts={concepts} depth={depth} />
560
- ) : null}
561
- {depth >= 4 && depth < 5 ? (
562
- <RelationshipLayer concepts={concepts} relationships={relationships} />
563
- ) : null}
564
- {depth >= 5 ? (
565
- <EmergentKnowledgeGraph
566
- model={graphModel}
567
- search={graphSearch}
568
- selectedId={selectedGraphId}
569
- onSearch={onGraphSearch}
570
- onSelect={onSelectGraphNode}
571
- />
572
- ) : null}
573
- </>
574
- );
575
- }
576
-
577
- function BrainOverviewPanel({
578
- memories,
579
- concepts,
580
- onOpenDepth,
581
- }: {
582
- memories: MemoryFragment[];
583
- concepts: KnowledgeConcept[];
584
- onOpenDepth: (depth: BrainDepth) => void;
585
- }) {
586
- const language = useAppStore((state) => state.language);
587
- const recent = memories.slice(0, 3);
588
- const older = memories.slice(3, 6);
589
- const topics = concepts.slice(0, 4);
590
-
591
- return (
592
- <section className="brain-overview-panel" aria-label="Brain overview">
593
- <div className="brain-overview-head">
594
- <div>
595
- <span>{t(language, "brain.overview.kicker")}</span>
596
- <strong>{t(language, "brain.overview.title")}</strong>
597
- </div>
598
- <button type="button" onClick={() => onOpenDepth(5)}>{t(language, "brain.overview.graph")}</button>
599
- </div>
600
- <div className="brain-overview-grid">
601
- <BrainOverviewColumn
602
- title={t(language, "brain.overview.recent")}
603
- empty={t(language, "brain.overview.recentEmpty")}
604
- items={recent.map((memory) => memory.title)}
605
- onOpen={() => onOpenDepth(2)}
606
- />
607
- <BrainOverviewColumn
608
- title={t(language, "brain.overview.older")}
609
- empty={t(language, "brain.overview.olderEmpty")}
610
- items={older.map((memory) => memory.title)}
611
- onOpen={() => onOpenDepth(2)}
612
- />
613
- <BrainOverviewColumn
614
- title={t(language, "brain.overview.topics")}
615
- empty={t(language, "brain.overview.topicsEmpty")}
616
- items={topics.map((concept) => concept.label)}
617
- onOpen={() => onOpenDepth(3)}
618
- />
619
- </div>
620
- </section>
621
- );
622
- }
623
-
624
- function BrainOverviewColumn({
625
- title,
626
- empty,
627
- items,
628
- onOpen,
629
- }: {
630
- title: string;
631
- empty: string;
632
- items: string[];
633
- onOpen: () => void;
634
- }) {
635
- return (
636
- <button type="button" className="brain-overview-column" onClick={onOpen}>
637
- <span>{title}</span>
638
- {items.length ? (
639
- items.slice(0, 3).map((item) => <strong key={item}>{item}</strong>)
640
- ) : (
641
- <em>{empty}</em>
642
- )}
643
- </button>
644
- );
645
- }
646
-
647
- function MemoryLayer({
648
- memories,
649
- depth,
650
- onRecallMemory,
651
- }: {
652
- memories: MemoryFragment[];
653
- depth: BrainDepth;
654
- onRecallMemory: (fragment: MemoryFragment) => void;
655
- }) {
656
- const visible = memories.slice(0, depth >= 3 ? 8 : 6);
657
- if (!visible.length) return <div className="memory-fragment is-empty">Memory is quiet</div>;
658
-
659
- return (
660
- <>
661
- {visible.map((memory, index) => {
662
- const point = polarPoint(index, visible.length, depth >= 3 ? 39 : 31, depth >= 3 ? 24 : 18, -112);
663
- return (
664
- <button
665
- key={memory.id}
666
- type="button"
667
- className="memory-fragment"
668
- style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 55}ms` })}
669
- onClick={() => onRecallMemory(memory)}
670
- >
671
- <span>{memory.kind}</span>
672
- <strong>{memory.title}</strong>
673
- </button>
674
- );
675
- })}
676
- </>
677
- );
678
- }
679
-
680
- function KnowledgeLayer({ concepts, depth }: { concepts: KnowledgeConcept[]; depth: BrainDepth }) {
681
- const visible = concepts.slice(0, depth >= 4 ? 10 : 7);
682
- if (!visible.length) return <div className="concept-signal is-empty">Knowledge is forming</div>;
683
-
684
- return (
685
- <>
686
- {visible.map((concept, index) => {
687
- const point = polarPoint(index, visible.length, 24, 15, -70);
688
- return (
689
- <button
690
- key={concept.id}
691
- type="button"
692
- className="concept-signal"
693
- style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 45}ms` })}
694
- title={concept.summary || concept.type}
695
- >
696
- <span>{concept.type}</span>
697
- {concept.label}
698
- </button>
699
- );
700
- })}
701
- </>
702
- );
703
- }
704
-
705
- function RelationshipLayer({
706
- concepts,
707
- relationships,
708
- }: {
709
- concepts: KnowledgeConcept[];
710
- relationships: RelationshipThread[];
711
- }) {
712
- const visibleConcepts = concepts.slice(0, 10);
713
- const layout = layoutGraphNodes(visibleConcepts, 30, 20);
714
- const positionById = new Map(layout.map((item) => [item.node.id, item]));
715
- const visibleRelationships = relationships
716
- .map((relationship, index) => {
717
- const source = positionById.get(relationship.source) || layout[index % Math.max(layout.length, 1)];
718
- const target = positionById.get(relationship.target) || layout[(index + 3) % Math.max(layout.length, 1)];
719
- return source && target && source.node.id !== target.node.id ? { relationship, source, target } : null;
720
- })
721
- .filter(Boolean)
722
- .slice(0, 8) as Array<{
723
- relationship: RelationshipThread;
724
- source: ReturnType<typeof layoutGraphNodes>[number];
725
- target: ReturnType<typeof layoutGraphNodes>[number];
726
- }>;
727
-
728
- if (!visibleRelationships.length) return null;
729
-
730
- return (
731
- <svg className="relationship-weave" viewBox="0 0 100 100" aria-hidden>
732
- {visibleRelationships.map(({ relationship, source, target }, index) => (
733
- <line
734
- key={`${relationship.id}-${index}`}
735
- x1={source.x}
736
- y1={source.y}
737
- x2={target.x}
738
- y2={target.y}
739
- style={{ animationDelay: `${index * 80}ms` }}
740
- />
741
- ))}
742
- </svg>
743
- );
744
- }
745
-
746
- function EmergentKnowledgeGraph({
747
- model,
748
- search,
749
- selectedId,
750
- onSearch,
751
- onSelect,
752
- }: {
753
- model: KnowledgeGraphModel;
754
- search: string;
755
- selectedId: string | null;
756
- onSearch: (value: string) => void;
757
- onSelect: (id: string | null) => void;
758
- }) {
759
- const language = useAppStore((state) => state.language);
760
- const query = search.trim().toLowerCase();
761
- const visibleNodes = React.useMemo(() => {
762
- const filtered = model.nodes.filter((node) => {
763
- if (!query) return true;
764
- return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
765
- });
766
- return filtered.slice(0, 18);
767
- }, [model.nodes, query]);
768
- const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
769
- const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
770
- const visibleEdges = React.useMemo(
771
- () => model.edges.filter((edge) => positionById.has(edge.source) && positionById.has(edge.target)).slice(0, 36),
772
- [model.edges, positionById],
773
- );
774
- const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
775
-
776
- return (
777
- <section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label="Knowledge Graph">
778
- <div className="brain-graph-head">
779
- <div>
780
- <span>Level 5</span>
781
- <strong>Knowledge Graph</strong>
782
- </div>
783
- <label className="brain-graph-search">
784
- <Search className="h-3.5 w-3.5" />
785
- <input
786
- value={search}
787
- onChange={(event) => onSearch(event.target.value)}
788
- placeholder="Search"
789
- aria-label="Search knowledge graph"
790
- />
791
- </label>
792
- </div>
793
-
794
- {visibleNodes.length ? (
795
- <div className="brain-graph-canvas">
796
- <svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
797
- {visibleEdges.map((edge, index) => {
798
- const source = positionById.get(edge.source);
799
- const target = positionById.get(edge.target);
800
- if (!source || !target) return null;
801
- return (
802
- <line
803
- key={`${edge.id}-${index}`}
804
- x1={source.x}
805
- y1={source.y}
806
- x2={target.x}
807
- y2={target.y}
808
- style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
809
- />
810
- );
811
- })}
812
- </svg>
813
- {layout.map(({ node, x, y }, index) => (
814
- <button
815
- key={node.id}
816
- type="button"
817
- className={`graph-node ${selected?.id === node.id ? "is-selected" : ""}`}
818
- style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
819
- onClick={() => onSelect(node.id)}
820
- >
821
- <span>{node.type}</span>
822
- {node.label}
823
- </button>
824
- ))}
825
- </div>
826
- ) : (
827
- <div className="brain-graph-empty">{t(language, "brain.graph.empty")}</div>
828
- )}
829
-
830
- <div className="brain-graph-focus">
831
- {selected ? (
832
- <>
833
- <span>{selected.type}</span>
834
- <strong>{selected.label}</strong>
835
- <p>{selected.summary || t(language, "brain.graph.summaryFallback")}</p>
836
- <p>{t(language, "brain.graph.focused")}</p>
837
- </>
838
- ) : (
839
- <p>{t(language, "brain.graph.emptyFocus")}</p>
840
- )}
841
- </div>
842
- </section>
843
- );
844
- }
845
-
846
- function buildMemoryFragments(memoryData: unknown, historyData: unknown): MemoryFragment[] {
847
- const memory = isRecord(memoryData) ? memoryData : {};
848
- const sourceRows = asArray<ApiRecord>(memory.sources).length
849
- ? asArray<ApiRecord>(memory.sources)
850
- : asArray<ApiRecord>(memory.tiers);
851
- const sourceFragments = sourceRows.map((item, index) => ({
852
- id: textValue(item, ["id", "source", "label"], `memory-${index}`),
853
- title: textValue(item, ["title", "label", "source", "path", "name"], "Workspace memory"),
854
- kind: titleValue(item, ["type", "source_type", "kind", "health"], "Memory"),
855
- }));
856
- const conversationFragments = asArray<ApiRecord>(historyData).map((item, index) => ({
857
- id: textValue(item, ["id", "conversation_id"], `conversation-${index}`),
858
- title: textValue(item, ["title", "summary", "id"], "Conversation"),
859
- kind: "Conversation",
860
- }));
861
-
862
- return uniqueById([...sourceFragments, ...conversationFragments]).slice(0, 10);
863
- }
864
-
865
- function parseKnowledgeGraph(data: unknown): KnowledgeGraphModel {
866
- const graph = isRecord(data) ? data : {};
867
- const rawNodes = asArray<ApiRecord>(graph.nodes);
868
- const rawEdges = asArray<ApiRecord>(graph.edges);
869
- const nodes = rawNodes.flatMap((node): KnowledgeConcept[] => {
870
- const id = textValue(node, ["id", "node_id", "title", "label"]);
871
- if (!id) return [];
872
- const metadata = isRecord(node.metadata) ? node.metadata : {};
873
- const type = titleValue(node, ["type", "kind", "category"], "Concept");
874
- const label = textValue(node, ["title", "label", "name"], id.replace(/^[^:]+:/, ""));
875
- const summary = textValue(node, ["summary", "description", "snippet"]) || textValue(metadata, ["summary", "description", "relative_path", "filename"]);
876
- const importance = clamp(numberValue(node, ["importance_norm", "importance", "score"]) || 0.5, 0.08, 1);
877
- return [{ id, label, type, summary, importance }];
878
- }).sort((left, right) => right.importance - left.importance);
879
- const ids = new Set(nodes.map((node) => node.id));
880
- const edges = rawEdges.flatMap((edge, index): RelationshipThread[] => {
881
- const source = textValue(edge, ["from", "source", "source_id"]);
882
- const target = textValue(edge, ["to", "target", "target_id"]);
883
- if (!source || !target || !ids.has(source) || !ids.has(target)) return [];
884
- return [{
885
- id: textValue(edge, ["id"], `edge-${index}`),
886
- source,
887
- target,
888
- label: titleValue(edge, ["type", "label", "relationship"], "Relates"),
889
- weight: numberValue(edge, ["weight", "score", "confidence"]) || 1,
890
- }];
891
- });
892
- return { nodes, edges };
893
- }
894
-
895
- function currentModelName(data: unknown) {
896
- const record = isRecord(data) ? data : {};
897
- const current = textValue(record, ["current", "current_model", "local_model"]);
898
- if (current) return current;
899
- const loaded = asArray<ApiRecord>(record.loaded || record.loaded_models);
900
- const firstLoaded = loaded.find((item) => item.id || item.name || item.model_id);
901
- return firstLoaded ? textValue(firstLoaded, ["name", "id", "model_id"], "local mind") : "local mind";
902
- }
903
-
904
- function portabilityLabel(data: unknown) {
905
- const record = isRecord(data) ? data : {};
906
- return textValue(record, ["archive_format", "format", "graph_schema_version", "schema_version"], ".latticebrain");
907
- }
908
-
909
- function backupHealthLabel(data: unknown) {
910
- const record = isRecord(data) ? data : {};
911
- const count = record.count || record.backups || record.available;
912
- if (count !== undefined && count !== null && count !== "") return `${count} backups`;
913
- return "Backups ready";
914
- }
915
-
916
- function summarizeCareResult(result: ApiResult) {
917
- if (!result.ok) return result.error || "Brain care action could not complete.";
918
- const data = isRecord(result.data) ? result.data : {};
919
- const directMessage = textValue(data, ["message", "status", "path", "archive_path", "backup_path", "export_path"]);
920
- if (directMessage) return directMessage;
921
- return "Brain care action completed.";
922
- }
923
-
924
- function stringValue(value: unknown, fallback = "") {
925
- if (typeof value === "string" && value.trim()) return value;
926
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
927
- if (typeof value === "boolean") return value ? "true" : "false";
928
- return fallback;
929
- }
930
-
931
- function fileToDataUrl(file: File) {
932
- return new Promise<string>((resolve, reject) => {
933
- const reader = new FileReader();
934
- reader.onload = () => resolve(String(reader.result || ""));
935
- reader.onerror = () => reject(reader.error);
936
- reader.readAsDataURL(file);
937
- });
938
- }
939
-
940
- function layoutGraphNodes(nodes: KnowledgeConcept[], radiusX: number, radiusY: number) {
941
- return nodes.map((node, index) => {
942
- const point = polarPoint(index, nodes.length, radiusX, radiusY, -88);
943
- return { node, x: point.x, y: point.y };
944
- });
945
- }
946
-
947
- function polarPoint(index: number, total: number, radiusX: number, radiusY: number, offsetDegrees = -90) {
948
- const count = Math.max(total, 1);
949
- const angle = ((360 / count) * index + offsetDegrees) * Math.PI / 180;
950
- return {
951
- x: 50 + Math.cos(angle) * radiusX,
952
- y: 50 + Math.sin(angle) * radiusY,
953
- };
954
- }
955
-
956
- function layerStyle(values: Record<string, string>) {
957
- return values as React.CSSProperties;
958
- }
959
-
960
- function uniqueById<T extends { id: string }>(items: T[]) {
961
- const seen = new Set<string>();
962
- return items.filter((item) => {
963
- if (seen.has(item.id)) return false;
964
- seen.add(item.id);
965
- return true;
966
- });
967
- }
968
-
969
- function isRecord(value: unknown): value is ApiRecord {
970
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
971
- }
972
-
973
- function textValue(record: ApiRecord, keys: string[], fallback = "") {
974
- for (const key of keys) {
975
- const value = record[key];
976
- if (typeof value === "string" && value.trim()) return value;
977
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
978
- }
979
- return fallback;
980
- }
981
-
982
- function titleValue(record: ApiRecord, keys: string[], fallback = "") {
983
- const value = textValue(record, keys, fallback);
984
- return value
985
- .replace(/[_-]+/g, " ")
986
- .replace(/\b\w/g, (character) => character.toUpperCase());
987
- }
988
-
989
- function numberValue(record: ApiRecord, keys: string[]) {
990
- for (const key of keys) {
991
- const value = Number(record[key]);
992
- if (Number.isFinite(value)) return value;
993
- }
994
- return 0;
995
- }
996
-
997
- function clamp(value: number, min: number, max: number) {
998
- return Math.max(min, Math.min(max, value));
999
- }