ltcai 4.7.2 → 5.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 (42) hide show
  1. package/README.md +59 -45
  2. package/docs/CHANGELOG.md +100 -0
  3. package/docs/TRUST_MODEL.md +66 -0
  4. package/docs/WHY_LATTICE.md +54 -0
  5. package/frontend/src/App.tsx +105 -70
  6. package/frontend/src/components/ProductFlow.tsx +102 -69
  7. package/frontend/src/components/primitives.tsx +1 -1
  8. package/frontend/src/i18n.ts +247 -0
  9. package/frontend/src/pages/System.tsx +1 -1
  10. package/frontend/src/store/appStore.ts +18 -0
  11. package/frontend/src/styles.css +36 -0
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/portability.py +11 -7
  14. package/lattice_brain/runtime/multi_agent.py +1 -1
  15. package/latticeai/__init__.py +1 -1
  16. package/latticeai/api/chat.py +19 -11
  17. package/latticeai/api/models.py +6 -0
  18. package/latticeai/api/security_dashboard.py +3 -15
  19. package/latticeai/api/static_routes.py +16 -0
  20. package/latticeai/app_factory.py +114 -40
  21. package/latticeai/core/audit.py +3 -1
  22. package/latticeai/core/builtin_hooks.py +7 -9
  23. package/latticeai/core/logging_safety.py +5 -21
  24. package/latticeai/core/marketplace.py +1 -1
  25. package/latticeai/core/security.py +67 -9
  26. package/latticeai/core/workspace_os.py +1 -1
  27. package/package.json +2 -2
  28. package/scripts/clean_release_artifacts.mjs +16 -1
  29. package/scripts/com.pts.claudecode.discord.plist +31 -0
  30. package/scripts/pts-claudecode-discord-bridge.mjs +189 -0
  31. package/scripts/run_integration_tests.mjs +91 -0
  32. package/scripts/start-pts-claudecode-discord.sh +51 -0
  33. package/src-tauri/Cargo.lock +1 -1
  34. package/src-tauri/Cargo.toml +1 -1
  35. package/src-tauri/tauri.conf.json +3 -2
  36. package/static/app/asset-manifest.json +5 -5
  37. package/static/app/assets/index-DONOJfMn.js +16 -0
  38. package/static/app/assets/index-DONOJfMn.js.map +1 -0
  39. package/static/app/assets/{index-KlQ04wVv.css → index-DuYYT2oh.css} +1 -1
  40. package/static/app/index.html +2 -2
  41. package/static/app/assets/index-DdAB4yfa.js +0 -16
  42. package/static/app/assets/index-DdAB4yfa.js.map +0 -1
@@ -23,6 +23,7 @@ import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/L
23
23
  import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
24
24
  import { useAppStore } from "@/store/appStore";
25
25
  import { asArray } from "@/lib/utils";
26
+ import { LANGUAGE_LABELS, t, type Language } from "@/i18n";
26
27
 
27
28
  type ApiRecord = Record<string, unknown>;
28
29
  type BrainDepth = 1 | 2 | 3 | 4 | 5;
@@ -68,21 +69,17 @@ const DEPTHS: Array<{ level: BrainDepth; label: string; state: BrainState }> = [
68
69
  { level: 5, label: "Knowledge Graph", state: "synthesizing" },
69
70
  ];
70
71
 
71
- const STARTER_PROMPTS = [
72
- "Remember this decision: ",
73
- "What do I already know about ",
74
- "Help me turn this project context into a plan: ",
75
- ];
76
-
77
72
  export default function App() {
78
73
  const theme = useAppStore((state) => state.theme);
74
+ const language = useAppStore((state) => state.language);
79
75
  const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
80
76
  const route = useHashRoute();
81
77
  const { state: brainState, intensity, setBrain } = useBrainState();
82
78
 
83
79
  React.useEffect(() => {
84
80
  document.documentElement.dataset.theme = theme;
85
- }, [theme]);
81
+ document.documentElement.lang = language === "ko" ? "ko" : "en";
82
+ }, [theme, language]);
86
83
 
87
84
  React.useEffect(() => {
88
85
  const onKey = (event: KeyboardEvent) => {
@@ -153,6 +150,7 @@ function BrainHome({
153
150
  onBrainChange: (state: BrainState, intensity?: number) => void;
154
151
  }) {
155
152
  const qc = useQueryClient();
153
+ const language = useAppStore((state) => state.language);
156
154
  const [messages, setMessages] = React.useState<Message[]>([]);
157
155
  const [draft, setDraft] = React.useState("");
158
156
  const [imageData, setImageData] = React.useState<string | null>(null);
@@ -185,6 +183,14 @@ function BrainHome({
185
183
  );
186
184
  const modelName = React.useMemo(() => currentModelName(modelsQ.data?.data), [modelsQ.data]);
187
185
  const currentDepth = DEPTHS[explorationDepth - 1];
186
+ const starterPrompts = React.useMemo(
187
+ () => [
188
+ t(language, "brain.prompt.remember"),
189
+ t(language, "brain.prompt.know"),
190
+ t(language, "brain.prompt.plan"),
191
+ ],
192
+ [language],
193
+ );
188
194
 
189
195
  React.useEffect(() => {
190
196
  if (streaming) onBrainChange("thinking", 0.94);
@@ -239,11 +245,11 @@ function BrainHome({
239
245
  if (result.error) {
240
246
  setMessages((items) => {
241
247
  const next = [...items];
242
- next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
248
+ next[next.length - 1] = { role: "assistant", content: `${t(language, "brain.unavailable")}: ${result.error}` };
243
249
  return next;
244
250
  });
245
251
  } else {
246
- setMemoryFeedback(`기억에 저장됨 · 연결된 주제 ${knowledgeConcepts.length}개 · 관련 기억 ${memoryFragments.length}개`);
252
+ setMemoryFeedback(t(language, "brain.saved", { topics: knowledgeConcepts.length, memories: memoryFragments.length }));
247
253
  }
248
254
  } finally {
249
255
  setStreaming(false);
@@ -282,7 +288,7 @@ function BrainHome({
282
288
  setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
283
289
  setMessages((items) => [
284
290
  ...items,
285
- { role: "assistant", content: `기억을 다시 꺼냈습니다: ${fragment.title}` },
291
+ { role: "assistant", content: t(language, "brain.recalled", { title: fragment.title }) },
286
292
  ]);
287
293
  }
288
294
 
@@ -300,15 +306,15 @@ function BrainHome({
300
306
  />
301
307
 
302
308
  <div className="brain-depth-badge" aria-live="polite">
303
- <span>Level {explorationDepth}</span>
304
- <strong>{currentDepth.label}</strong>
309
+ <span>{t(language, "brain.level")} {explorationDepth}</span>
310
+ <strong>{t(language, `brain.depth.${explorationDepth}`)}</strong>
305
311
  </div>
306
312
 
307
313
  <div className="brain-depth-actions" aria-label="Brain quick views">
308
- <button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>기억 보기</button>
309
- <button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>주제 보기</button>
310
- <button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>관계 보기</button>
311
- <button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>그래프로 보기</button>
314
+ <button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>{t(language, "brain.view.memories")}</button>
315
+ <button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>{t(language, "brain.view.topics")}</button>
316
+ <button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>{t(language, "brain.view.relationships")}</button>
317
+ <button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>{t(language, "brain.view.graph")}</button>
312
318
  </div>
313
319
 
314
320
  <div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
@@ -328,7 +334,7 @@ function BrainHome({
328
334
 
329
335
  {explorationDepth > 1 ? (
330
336
  <button className="brain-surface-control" type="button" onClick={surface}>
331
- Surface
337
+ {t(language, "brain.surface")}
332
338
  </button>
333
339
  ) : null}
334
340
  </div>
@@ -337,18 +343,19 @@ function BrainHome({
337
343
  <section className="brain-conversation" aria-label="Conversation">
338
344
  <div className="brain-conversation-header">
339
345
  <div>
340
- <h1>Lattice Brain</h1>
341
- <span>{currentDepth.label}</span>
346
+ <h1>{t(language, "brain.title")}</h1>
347
+ <span>{t(language, `brain.depth.${explorationDepth}`)}</span>
342
348
  </div>
349
+ <LanguageSwitcher compact />
343
350
  <div className="brain-ownership-strip" aria-label="Brain ownership guarantees">
344
- <span>Local-first</span>
345
- <span>Portable</span>
346
- <span>Private</span>
351
+ <span>{t(language, "brain.local")}</span>
352
+ <span>{t(language, "brain.portable")}</span>
353
+ <span>{t(language, "brain.private")}</span>
347
354
  </div>
348
355
  <div>{modelName}</div>
349
356
  <button className="brain-admin-link" type="button" onClick={() => navigateHash("/admin")}>
350
357
  <ShieldCheck className="h-3.5 w-3.5" />
351
- Admin
358
+ {t(language, "brain.admin")}
352
359
  </button>
353
360
  </div>
354
361
 
@@ -360,13 +367,11 @@ function BrainHome({
360
367
  />
361
368
  {messages.length === 0 ? (
362
369
  <div className="mind-empty">
363
- <div className="mind-empty-kicker">내 오래가는 기억</div>
364
- <div className="mind-empty-title">잊으면 안 되는 일부터 말해 주세요.</div>
365
- <p>
366
- 문서, 대화, 프로젝트, 결정이 Brain에 쌓이고 나중에 주제와 관계로 다시 보입니다.
367
- </p>
370
+ <div className="mind-empty-kicker">{t(language, "brain.empty.kicker")}</div>
371
+ <div className="mind-empty-title">{t(language, "brain.empty.title")}</div>
372
+ <p>{t(language, "brain.empty.body")}</p>
368
373
  <div className="mind-empty-prompts" aria-label="Starter prompts">
369
- {STARTER_PROMPTS.map((prompt) => (
374
+ {starterPrompts.map((prompt) => (
370
375
  <button key={prompt} type="button" onClick={() => setDraft(prompt)}>
371
376
  {prompt}
372
377
  </button>
@@ -384,7 +389,7 @@ function BrainHome({
384
389
 
385
390
  {memoryFeedback ? <div className="brain-save-feedback" role="status">{memoryFeedback}</div> : null}
386
391
 
387
- <BrainCarePanel />
392
+ <BrainCarePanel language={language} />
388
393
 
389
394
  <div className="brain-composer">
390
395
  <textarea
@@ -396,12 +401,12 @@ function BrainHome({
396
401
  void send();
397
402
  }
398
403
  }}
399
- placeholder="Talk to your Brain..."
404
+ placeholder={t(language, "brain.placeholder")}
400
405
  />
401
406
  <div className="brain-composer-actions">
402
407
  <label className="brain-image-input">
403
408
  <ImagePlus className="h-3.5 w-3.5" />
404
- <span>Image</span>
409
+ <span>{t(language, "brain.image")}</span>
405
410
  <input
406
411
  type="file"
407
412
  accept="image/*"
@@ -412,9 +417,9 @@ function BrainHome({
412
417
  }}
413
418
  />
414
419
  </label>
415
- {imageData ? <span className="brain-quiet-success">Image attached</span> : null}
420
+ {imageData ? <span className="brain-quiet-success">{t(language, "brain.imageAttached")}</span> : null}
416
421
  <Button onClick={() => void send()} disabled={!draft.trim() || streaming} className="rounded-full px-5">
417
- <Send className="h-4 w-4" /> Send
422
+ <Send className="h-4 w-4" /> {t(language, "brain.send")}
418
423
  </Button>
419
424
  </div>
420
425
  </div>
@@ -423,8 +428,30 @@ function BrainHome({
423
428
  );
424
429
  }
425
430
 
431
+ function LanguageSwitcher({ compact = false }: { compact?: boolean }) {
432
+ const language = useAppStore((state) => state.language);
433
+ const setLanguage = useAppStore((state) => state.setLanguage);
434
+
435
+ return (
436
+ <div className={compact ? "language-switcher compact" : "language-switcher"} aria-label={t(language, "language.label")}>
437
+ {(["ko", "en"] as Language[]).map((item) => (
438
+ <button
439
+ key={item}
440
+ type="button"
441
+ className={language === item ? "is-active" : ""}
442
+ onClick={() => setLanguage(item)}
443
+ aria-pressed={language === item}
444
+ >
445
+ {LANGUAGE_LABELS[item]}
446
+ </button>
447
+ ))}
448
+ </div>
449
+ );
450
+ }
451
+
426
452
  function AdminConsole({ onBack }: { onBack: () => void }) {
427
453
  const qc = useQueryClient();
454
+ const language = useAppStore((state) => state.language);
428
455
  const [filters, setFilters] = React.useState<AdminFilterState>({ q: "", actor: "", action: "", severity: "", limit: 50 });
429
456
  const { summaryQ, statsQ, usersQ, auditQ, securityQ, securityEventsQ, policiesQ, rolesQ, retentionQ, indexQ } = useAdminConsoleData(filters);
430
457
  const rebuildIndex = useMutation({
@@ -444,13 +471,14 @@ function AdminConsole({ onBack }: { onBack: () => void }) {
444
471
  <header className="admin-console-header">
445
472
  <button className="admin-back-button" type="button" onClick={onBack}>
446
473
  <ArrowLeft className="h-4 w-4" />
447
- Brain
474
+ {t(language, "admin.back")}
448
475
  </button>
449
476
  <div>
450
- <span>Separate admin workspace</span>
451
- <h1>Admin Console</h1>
452
- <p>Users, logs, security, and Brain health stay out of the normal user experience.</p>
477
+ <span>{t(language, "admin.kicker")}</span>
478
+ <h1>{t(language, "admin.title")}</h1>
479
+ <p>{t(language, "admin.body")}</p>
453
480
  </div>
481
+ <LanguageSwitcher compact />
454
482
  </header>
455
483
 
456
484
  <section className="admin-metrics" aria-label="Admin overview">
@@ -643,7 +671,7 @@ function AdminList({ items, empty, render }: { items: unknown[]; empty: string;
643
671
  return <div className="admin-list">{items.map((item, index) => <div key={index} className="admin-list-row">{render(item)}</div>)}</div>;
644
672
  }
645
673
 
646
- function BrainCarePanel() {
674
+ function BrainCarePanel({ language }: { language: Language }) {
647
675
  const qc = useQueryClient();
648
676
  const [expanded, setExpanded] = React.useState(false);
649
677
  const [archivePath, setArchivePath] = React.useState("");
@@ -678,7 +706,7 @@ function BrainCarePanel() {
678
706
  const backupStatus = backupHealthLabel(backupHealthQ.data?.data);
679
707
 
680
708
  return (
681
- <section className={`brain-care-panel ${expanded ? "is-expanded" : "is-collapsed"}`} aria-label="Care for my Brain">
709
+ <section className={`brain-care-panel ${expanded ? "is-expanded" : "is-collapsed"}`} aria-label={t(language, "care.title")}>
682
710
  <button
683
711
  className="brain-care-summary"
684
712
  type="button"
@@ -687,11 +715,11 @@ function BrainCarePanel() {
687
715
  onClick={() => setExpanded((value) => !value)}
688
716
  >
689
717
  <span className="brain-care-summary-main">
690
- <span><ShieldCheck className="h-3.5 w-3.5" /> Care for my Brain</span>
691
- <strong>Own it locally. Keep it portable.</strong>
718
+ <span><ShieldCheck className="h-3.5 w-3.5" /> {t(language, "care.title")}</span>
719
+ <strong>{t(language, "care.subtitle")}</strong>
692
720
  </span>
693
721
  <div className="brain-care-proof" aria-label="Ownership model">
694
- <span>Private</span>
722
+ <span>{t(language, "care.private")}</span>
695
723
  <span>{portableFormat}</span>
696
724
  <span>{backupStatus}</span>
697
725
  </div>
@@ -703,22 +731,25 @@ function BrainCarePanel() {
703
731
  <div className="brain-care-actions">
704
732
  <CareButton
705
733
  icon={<Download className="h-3.5 w-3.5" />}
706
- label="Export"
707
- detail="Take it with you"
734
+ label={t(language, "care.export")}
735
+ detail={t(language, "care.export.detail")}
736
+ pendingLabel={t(language, "care.working")}
708
737
  pending={exportGraph.isPending}
709
738
  onClick={() => exportGraph.mutate()}
710
739
  />
711
740
  <CareButton
712
741
  icon={<DatabaseBackup className="h-3.5 w-3.5" />}
713
- label="Backup"
714
- detail="Save a copy"
742
+ label={t(language, "care.backup")}
743
+ detail={t(language, "care.backup.detail")}
744
+ pendingLabel={t(language, "care.working")}
715
745
  pending={backupGraph.isPending}
716
746
  onClick={() => backupGraph.mutate()}
717
747
  />
718
748
  <CareButton
719
749
  icon={<Archive className="h-3.5 w-3.5" />}
720
- label="Archive"
721
- detail="Encrypted Brain"
750
+ label={t(language, "care.archive")}
751
+ detail={t(language, "care.archive.detail")}
752
+ pendingLabel={t(language, "care.working")}
722
753
  pending={archiveBrain.isPending}
723
754
  disabled={!passphrase.trim()}
724
755
  onClick={() => archiveBrain.mutate()}
@@ -729,15 +760,15 @@ function BrainCarePanel() {
729
760
  <input
730
761
  value={archivePath}
731
762
  onChange={(event) => setArchivePath(event.target.value)}
732
- placeholder="Paste an archive path to inspect or preview"
733
- aria-label="Brain archive path"
763
+ placeholder={t(language, "care.path.placeholder")}
764
+ aria-label={t(language, "care.path.label")}
734
765
  />
735
766
  <input
736
767
  type="password"
737
768
  value={passphrase}
738
769
  onChange={(event) => setPassphrase(event.target.value)}
739
- placeholder="Archive passphrase"
740
- aria-label="Brain archive passphrase"
770
+ placeholder={t(language, "care.passphrase.placeholder")}
771
+ aria-label={t(language, "care.passphrase.label")}
741
772
  />
742
773
  <div className="brain-care-archive-actions">
743
774
  <Button
@@ -746,7 +777,7 @@ function BrainCarePanel() {
746
777
  disabled={!archivePath.trim() || inspectArchive.isPending}
747
778
  onClick={() => inspectArchive.mutate()}
748
779
  >
749
- <Eye className="h-3.5 w-3.5" /> Inspect
780
+ <Eye className="h-3.5 w-3.5" /> {t(language, "care.inspect")}
750
781
  </Button>
751
782
  <Button
752
783
  variant="outline"
@@ -754,7 +785,7 @@ function BrainCarePanel() {
754
785
  disabled={!archivePath.trim() || !passphrase.trim() || restorePreview.isPending}
755
786
  onClick={() => restorePreview.mutate()}
756
787
  >
757
- <RotateCcw className="h-3.5 w-3.5" /> Restore preview
788
+ <RotateCcw className="h-3.5 w-3.5" /> {t(language, "care.restorePreview")}
758
789
  </Button>
759
790
  </div>
760
791
  </div>
@@ -765,7 +796,7 @@ function BrainCarePanel() {
765
796
  </div>
766
797
  ) : (
767
798
  <p className="brain-care-note">
768
- Restore preview checks an archive without changing your Brain. Confirmed restore stays in Settings.
799
+ {t(language, "care.note")}
769
800
  </p>
770
801
  )}
771
802
  </div>
@@ -792,6 +823,7 @@ function CareButton({
792
823
  icon,
793
824
  label,
794
825
  detail,
826
+ pendingLabel,
795
827
  pending,
796
828
  disabled,
797
829
  onClick,
@@ -799,6 +831,7 @@ function CareButton({
799
831
  icon: React.ReactNode;
800
832
  label: string;
801
833
  detail: string;
834
+ pendingLabel: string;
802
835
  pending?: boolean;
803
836
  disabled?: boolean;
804
837
  onClick: () => void;
@@ -807,7 +840,7 @@ function CareButton({
807
840
  <button className="brain-care-button" type="button" disabled={disabled || pending} onClick={onClick}>
808
841
  {icon}
809
842
  <span>
810
- <strong>{pending ? "Working" : label}</strong>
843
+ <strong>{pending ? pendingLabel : label}</strong>
811
844
  <small>{detail}</small>
812
845
  </span>
813
846
  </button>
@@ -872,6 +905,7 @@ function BrainOverviewPanel({
872
905
  concepts: KnowledgeConcept[];
873
906
  onOpenDepth: (depth: BrainDepth) => void;
874
907
  }) {
908
+ const language = useAppStore((state) => state.language);
875
909
  const recent = memories.slice(0, 3);
876
910
  const older = memories.slice(3, 6);
877
911
  const topics = concepts.slice(0, 4);
@@ -880,27 +914,27 @@ function BrainOverviewPanel({
880
914
  <section className="brain-overview-panel" aria-label="Brain overview">
881
915
  <div className="brain-overview-head">
882
916
  <div>
883
- <span>Brain 한눈에 보기</span>
884
- <strong>기억과 주제를 바로 확인하세요.</strong>
917
+ <span>{t(language, "brain.overview.kicker")}</span>
918
+ <strong>{t(language, "brain.overview.title")}</strong>
885
919
  </div>
886
- <button type="button" onClick={() => onOpenDepth(5)}>전체 그래프</button>
920
+ <button type="button" onClick={() => onOpenDepth(5)}>{t(language, "brain.overview.graph")}</button>
887
921
  </div>
888
922
  <div className="brain-overview-grid">
889
923
  <BrainOverviewColumn
890
- title="최근 기억"
891
- empty="아직 최근 기억이 없습니다."
924
+ title={t(language, "brain.overview.recent")}
925
+ empty={t(language, "brain.overview.recentEmpty")}
892
926
  items={recent.map((memory) => memory.title)}
893
927
  onOpen={() => onOpenDepth(2)}
894
928
  />
895
929
  <BrainOverviewColumn
896
- title="이전 기억"
897
- empty="대화가 쌓이면 과거 기억이 보입니다."
930
+ title={t(language, "brain.overview.older")}
931
+ empty={t(language, "brain.overview.olderEmpty")}
898
932
  items={older.map((memory) => memory.title)}
899
933
  onOpen={() => onOpenDepth(2)}
900
934
  />
901
935
  <BrainOverviewColumn
902
- title="주요 주제"
903
- empty="주제가 형성되는 중입니다."
936
+ title={t(language, "brain.overview.topics")}
937
+ empty={t(language, "brain.overview.topicsEmpty")}
904
938
  items={topics.map((concept) => concept.label)}
905
939
  onOpen={() => onOpenDepth(3)}
906
940
  />
@@ -1044,6 +1078,7 @@ function EmergentKnowledgeGraph({
1044
1078
  onSearch: (value: string) => void;
1045
1079
  onSelect: (id: string | null) => void;
1046
1080
  }) {
1081
+ const language = useAppStore((state) => state.language);
1047
1082
  const query = search.trim().toLowerCase();
1048
1083
  const visibleNodes = React.useMemo(() => {
1049
1084
  const filtered = model.nodes.filter((node) => {
@@ -1111,7 +1146,7 @@ function EmergentKnowledgeGraph({
1111
1146
  ))}
1112
1147
  </div>
1113
1148
  ) : (
1114
- <div className="brain-graph-empty">No matching knowledge yet</div>
1149
+ <div className="brain-graph-empty">{t(language, "brain.graph.empty")}</div>
1115
1150
  )}
1116
1151
 
1117
1152
  <div className="brain-graph-focus">
@@ -1119,11 +1154,11 @@ function EmergentKnowledgeGraph({
1119
1154
  <>
1120
1155
  <span>{selected.type}</span>
1121
1156
  <strong>{selected.label}</strong>
1122
- <p>{selected.summary || "This concept is part of the deepest knowledge layer."}</p>
1123
- <p>대화와 문서에서 함께 나온 내용이 선으로 이어집니다.</p>
1157
+ <p>{selected.summary || t(language, "brain.graph.summaryFallback")}</p>
1158
+ <p>{t(language, "brain.graph.focused")}</p>
1124
1159
  </>
1125
1160
  ) : (
1126
- <p>대화, 문서, 프로젝트를 쌓으면 Brain 그래프가 자랍니다.</p>
1161
+ <p>{t(language, "brain.graph.emptyFocus")}</p>
1127
1162
  )}
1128
1163
  </div>
1129
1164
  </section>