ltcai 6.4.0 → 6.6.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 (40) hide show
  1. package/README.md +31 -28
  2. package/docs/CHANGELOG.md +62 -0
  3. package/frontend/openapi.json +65 -1
  4. package/frontend/src/api/client.ts +2 -0
  5. package/frontend/src/api/openapi.ts +86 -0
  6. package/frontend/src/features/brain/BrainComposer.tsx +20 -1
  7. package/frontend/src/features/brain/BrainConversation.tsx +48 -4
  8. package/frontend/src/features/brain/BrainHome.tsx +61 -1
  9. package/frontend/src/features/brain/BrainMemoryLayer.tsx +8 -1
  10. package/frontend/src/features/brain/BrainOverviewPanel.tsx +76 -1
  11. package/frontend/src/features/brain/brainData.ts +125 -1
  12. package/frontend/src/features/brain/types.ts +52 -0
  13. package/frontend/src/i18n.ts +66 -0
  14. package/frontend/src/styles.css +261 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/ingest.py +41 -6
  17. package/lattice_brain/ingestion.py +1 -0
  18. package/lattice_brain/runtime/multi_agent.py +1 -1
  19. package/latticeai/__init__.py +1 -1
  20. package/latticeai/api/knowledge_graph.py +11 -0
  21. package/latticeai/api/memory.py +20 -0
  22. package/latticeai/app_factory.py +2 -0
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/runtime/chat_wiring.py +2 -0
  26. package/latticeai/runtime/router_registration.py +3 -0
  27. package/latticeai/services/memory_service.py +188 -2
  28. package/latticeai/services/router_context.py +1 -0
  29. package/latticeai/services/upload_service.py +12 -0
  30. package/package.json +1 -1
  31. package/src-tauri/Cargo.lock +1 -1
  32. package/src-tauri/Cargo.toml +1 -1
  33. package/src-tauri/tauri.conf.json +1 -1
  34. package/static/app/asset-manifest.json +5 -5
  35. package/static/app/assets/{index-Div5vMlq.css → index-C8toxDpv.css} +1 -1
  36. package/static/app/assets/index-CbvcAQ6B.js +16 -0
  37. package/static/app/assets/index-CbvcAQ6B.js.map +1 -0
  38. package/static/app/index.html +2 -2
  39. package/static/app/assets/index-t1jx1BR9.js +0 -16
  40. package/static/app/assets/index-t1jx1BR9.js.map +0 -1
@@ -5,7 +5,7 @@ import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/L
5
5
  import { useAppStore } from "@/store/appStore";
6
6
  import { t } from "@/i18n";
7
7
  import { BrainConversation } from "./BrainConversation";
8
- import { buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
8
+ import { buildBrainProof, buildBrainReadiness, buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
9
9
  import { DepthEmergence } from "./DepthEmergence";
10
10
  import { DEPTHS, type BrainDepth, type MemoryFragment, type Message } from "./types";
11
11
 
@@ -29,6 +29,8 @@ export function BrainHome({
29
29
  const [graphSearch, setGraphSearch] = React.useState("");
30
30
  const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
31
31
  const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
32
+ const [uploadingDocument, setUploadingDocument] = React.useState(false);
33
+ const [lastRecallQuery, setLastRecallQuery] = React.useState("");
32
34
  const streamRef = React.useRef<HTMLDivElement>(null);
33
35
  const recallTimerRef = React.useRef<number | null>(null);
34
36
 
@@ -36,6 +38,10 @@ export function BrainHome({
36
38
  const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
37
39
  const graphQ = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
38
40
  const modelsQ = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
41
+ const brainProofQ = useQuery({
42
+ queryKey: ["memoryBrainProof", lastRecallQuery],
43
+ queryFn: () => latticeApi.memoryBrainProof(lastRecallQuery, 3),
44
+ });
39
45
 
40
46
  const memoryFragments = React.useMemo(
41
47
  () => buildMemoryFragments(memoriesQ.data?.data, historyQ.data?.data),
@@ -46,11 +52,19 @@ export function BrainHome({
46
52
  () => graphModel.nodes.slice(0, 10),
47
53
  [graphModel.nodes],
48
54
  );
55
+ const brainReadiness = React.useMemo(
56
+ () => buildBrainReadiness(memoriesQ.data?.data, memoryFragments.length, knowledgeConcepts.length),
57
+ [knowledgeConcepts.length, memoriesQ.data, memoryFragments.length],
58
+ );
49
59
  const relationshipThreads = React.useMemo(
50
60
  () => graphModel.edges.slice(0, 10),
51
61
  [graphModel.edges],
52
62
  );
53
63
  const modelName = React.useMemo(() => currentModelName(modelsQ.data?.data), [modelsQ.data]);
64
+ const brainProof = React.useMemo(
65
+ () => buildBrainProof(brainProofQ.data?.data, modelName),
66
+ [brainProofQ.data, modelName],
67
+ );
54
68
  const currentDepth = DEPTHS[explorationDepth - 1];
55
69
  const starterPrompts = React.useMemo(
56
70
  () => [
@@ -119,12 +133,39 @@ export function BrainHome({
119
133
  });
120
134
  } else {
121
135
  setMemoryFeedback(t(language, "brain.saved", { topics: knowledgeConcepts.length, memories: memoryFragments.length }));
136
+ setLastRecallQuery(text);
122
137
  }
123
138
  } finally {
124
139
  setStreaming(false);
125
140
  void qc.invalidateQueries({ queryKey: ["chatHistory"] });
126
141
  void qc.invalidateQueries({ queryKey: ["memoryManager"] });
127
142
  void qc.invalidateQueries({ queryKey: ["graph"] });
143
+ void qc.invalidateQueries({ queryKey: ["memoryBrainProof"] });
144
+ }
145
+ }
146
+
147
+ async function uploadDocument(file: File) {
148
+ if (uploadingDocument) return;
149
+
150
+ setUploadingDocument(true);
151
+ setMemoryFeedback(t(language, "brain.upload.pending", { name: file.name }));
152
+ onBrainChange("recalling", 0.86);
153
+
154
+ try {
155
+ const result = await latticeApi.uploadDocument(file);
156
+ if (result.error) {
157
+ setMemoryFeedback(t(language, "brain.upload.failed", { reason: result.error }));
158
+ return;
159
+ }
160
+
161
+ setMemoryFeedback(t(language, "brain.upload.saved", { name: file.name }));
162
+ setLastRecallQuery(file.name);
163
+ triggerBrainRecall();
164
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
165
+ void qc.invalidateQueries({ queryKey: ["graph"] });
166
+ void qc.invalidateQueries({ queryKey: ["memoryBrainProof"] });
167
+ } finally {
168
+ setUploadingDocument(false);
128
169
  }
129
170
  }
130
171
 
@@ -186,6 +227,21 @@ export function BrainHome({
186
227
  <button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>{t(language, "brain.view.graph")}</button>
187
228
  </div>
188
229
 
230
+ <div className="brain-depth-rail" aria-label={t(language, "brain.depthRail.aria")}>
231
+ {DEPTHS.map((depth) => (
232
+ <button
233
+ key={depth.level}
234
+ type="button"
235
+ className={depth.level <= explorationDepth ? "is-revealed" : ""}
236
+ aria-current={depth.level === explorationDepth ? "step" : undefined}
237
+ onClick={() => jumpToDepth(depth.level)}
238
+ >
239
+ <span>{depth.level}</span>
240
+ <strong>{t(language, `brain.depth.${depth.level}`)}</strong>
241
+ </button>
242
+ ))}
243
+ </div>
244
+
189
245
  <div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
190
246
  <DepthEmergence
191
247
  depth={explorationDepth}
@@ -222,9 +278,13 @@ export function BrainHome({
222
278
  streamRef={streamRef}
223
279
  memories={memoryFragments}
224
280
  concepts={knowledgeConcepts}
281
+ readiness={brainReadiness}
282
+ proof={brainProof}
283
+ uploadingDocument={uploadingDocument}
225
284
  onOpenDepth={jumpToDepth}
226
285
  onDraftChange={setDraft}
227
286
  onImageDataChange={setImageData}
287
+ onUploadDocument={(file) => void uploadDocument(file)}
228
288
  onSend={() => void send()}
229
289
  />
230
290
  </main>
@@ -14,7 +14,14 @@ export function BrainMemoryLayer({
14
14
  }) {
15
15
  const language = useAppStore((state) => state.language);
16
16
  const visible = memories.slice(0, depth >= 3 ? 8 : 6);
17
- if (!visible.length) return <div className="memory-fragment is-empty">{t(language, "brain.memory.empty")}</div>;
17
+ if (!visible.length) {
18
+ return (
19
+ <div className="memory-fragment is-empty">
20
+ <span>{t(language, "brain.memory.empty.kicker")}</span>
21
+ <strong>{t(language, "brain.memory.empty")}</strong>
22
+ </div>
23
+ );
24
+ }
18
25
 
19
26
  return (
20
27
  <>
@@ -1,21 +1,31 @@
1
1
  import * as React from "react";
2
+ import { BrainCircuit, DatabaseZap, Repeat2 } from "lucide-react";
2
3
  import { t } from "@/i18n";
3
4
  import { useAppStore } from "@/store/appStore";
4
- import type { BrainDepth, KnowledgeConcept, MemoryFragment } from "./types";
5
+ import type { BrainDepth, BrainProof, BrainReadiness, KnowledgeConcept, MemoryFragment } from "./types";
5
6
 
6
7
  export function BrainOverviewPanel({
7
8
  memories,
8
9
  concepts,
10
+ readiness,
11
+ proof,
9
12
  onOpenDepth,
10
13
  }: {
11
14
  memories: MemoryFragment[];
12
15
  concepts: KnowledgeConcept[];
16
+ readiness: BrainReadiness;
17
+ proof: BrainProof;
13
18
  onOpenDepth: (depth: BrainDepth) => void;
14
19
  }) {
15
20
  const language = useAppStore((state) => state.language);
16
21
  const recent = memories.slice(0, 3);
17
22
  const older = memories.slice(3, 6);
18
23
  const topics = concepts.slice(0, 4);
24
+ const hasDurableProof = proof.proofs.hasDurableEvidence || proof.modelContinuity.proven;
25
+ const hasRecallProof = proof.recall.items.length > 0;
26
+ const modelProofValue = proof.modelContinuity.proven && proof.claims.keepsContextAcrossModels
27
+ ? proof.modelContinuity.activeModel || t(language, "brain.proof.noModel")
28
+ : t(language, "brain.proof.modelPending");
19
29
 
20
30
  return (
21
31
  <section className="brain-overview-panel" aria-label={t(language, "brain.aria.overview")}>
@@ -46,10 +56,75 @@ export function BrainOverviewPanel({
46
56
  onOpen={() => onOpenDepth(3)}
47
57
  />
48
58
  </div>
59
+ <div className="brain-readiness-strip" data-state={readiness.state}>
60
+ <div>
61
+ <span>{t(language, "brain.readiness.kicker")}</span>
62
+ <strong>{t(language, readiness.titleKey)}</strong>
63
+ </div>
64
+ <div className="brain-readiness-meter" aria-label={t(language, "brain.readiness.aria")}>
65
+ <i style={{ width: `${readiness.score}%` }} />
66
+ </div>
67
+ <button type="button" onClick={() => onOpenDepth(readiness.depth)}>
68
+ {t(language, readiness.actionKey)}
69
+ </button>
70
+ </div>
71
+ <div className="brain-proof-strip" aria-label={t(language, "brain.proof.aria")}>
72
+ <BrainProofPoint
73
+ icon={<DatabaseZap className="h-4 w-4" />}
74
+ label={t(language, "brain.proof.context")}
75
+ value={hasDurableProof
76
+ ? t(language, "brain.proof.contextValue", { count: proof.proofs.durableItems })
77
+ : t(language, "brain.proof.contextEmpty")}
78
+ />
79
+ <BrainProofPoint
80
+ icon={<Repeat2 className="h-4 w-4" />}
81
+ label={t(language, "brain.proof.model")}
82
+ value={modelProofValue}
83
+ />
84
+ <BrainProofPoint
85
+ icon={<BrainCircuit className="h-4 w-4" />}
86
+ label={t(language, "brain.proof.store")}
87
+ value={t(language, "brain.proof.storeValue", {
88
+ topics: proof.proofs.graphConcepts,
89
+ vectors: proof.proofs.vectorItems,
90
+ })}
91
+ />
92
+ </div>
93
+ {hasRecallProof ? (
94
+ <button type="button" className="brain-recall-proof" onClick={() => onOpenDepth(2)}>
95
+ <span>{t(language, "brain.proof.recall")}</span>
96
+ <strong>{proof.recall.items[0].title}</strong>
97
+ <small>{proof.recall.items[0].snippet || proof.recall.query}</small>
98
+ </button>
99
+ ) : (
100
+ <div className="brain-recall-proof is-empty" role="status">
101
+ <span>{t(language, "brain.proof.recallEmpty.kicker")}</span>
102
+ <strong>{t(language, hasDurableProof ? "brain.proof.recallPending" : "brain.proof.recallEmpty.title")}</strong>
103
+ <small>{t(language, hasDurableProof ? "brain.proof.recallPending.detail" : "brain.proof.recallEmpty.detail")}</small>
104
+ </div>
105
+ )}
49
106
  </section>
50
107
  );
51
108
  }
52
109
 
110
+ function BrainProofPoint({
111
+ icon,
112
+ label,
113
+ value,
114
+ }: {
115
+ icon: React.ReactNode;
116
+ label: string;
117
+ value: string;
118
+ }) {
119
+ return (
120
+ <div className="brain-proof-point">
121
+ {icon}
122
+ <span>{label}</span>
123
+ <strong>{value}</strong>
124
+ </div>
125
+ );
126
+ }
127
+
53
128
  function BrainOverviewColumn({
54
129
  title,
55
130
  empty,
@@ -1,5 +1,5 @@
1
1
  import { asArray } from "@/lib/utils";
2
- import type { ApiRecord, KnowledgeConcept, KnowledgeGraphModel, MemoryFragment, RelationshipThread } from "./types";
2
+ import type { ApiRecord, BrainDepth, BrainProof, BrainReadiness, KnowledgeConcept, KnowledgeGraphModel, MemoryFragment, RelationshipThread } from "./types";
3
3
  import { clamp } from "./graphLayout";
4
4
 
5
5
  export function buildMemoryFragments(memoryData: unknown, historyData: unknown): MemoryFragment[] {
@@ -60,6 +60,77 @@ export function currentModelName(data: unknown) {
60
60
  return firstLoaded ? textValue(firstLoaded, ["name", "id", "model_id"], "local mind") : "local mind";
61
61
  }
62
62
 
63
+ export function buildBrainReadiness(memoryData: unknown, fallbackMemoryCount: number, fallbackConceptCount: number): BrainReadiness {
64
+ const memory = isRecord(memoryData) ? memoryData : {};
65
+ const backend = isRecord(memory.brain_readiness) ? memory.brain_readiness : {};
66
+ const backendState = textValue(backend, ["state"]);
67
+ const backendDepth = numberValue(backend, ["depth"]);
68
+ const backendScore = numberValue(backend, ["score"]);
69
+ if ((backendState === "quiet" || backendState === "forming" || backendState === "alive") && isBrainDepth(backendDepth)) {
70
+ const signals = isRecord(backend.signals) ? backend.signals : {};
71
+ return {
72
+ score: clamp(Math.round(backendScore || 0), 0, 100),
73
+ state: backendState,
74
+ depth: backendDepth,
75
+ titleKey: textValue(backend, ["title_key", "titleKey"], `brain.readiness.${backendState}`),
76
+ actionKey: textValue(backend, ["action_key", "actionKey"], readinessActionKey(backendState)),
77
+ source: "memory_service",
78
+ signals: {
79
+ memoryCount: numberValue(signals, ["memory_count", "memoryCount"]),
80
+ conceptCount: numberValue(signals, ["concept_count", "conceptCount"]),
81
+ relationshipCount: numberValue(signals, ["relationship_count", "relationshipCount"]),
82
+ healthySources: numberValue(signals, ["healthy_sources", "healthySources"]),
83
+ },
84
+ };
85
+ }
86
+ return fallbackBrainReadiness(fallbackMemoryCount, fallbackConceptCount);
87
+ }
88
+
89
+ export function buildBrainProof(data: unknown, fallbackModelName = ""): BrainProof {
90
+ const proof = isRecord(data) ? data : {};
91
+ const modelContinuity = isRecord(proof.model_continuity) ? proof.model_continuity : {};
92
+ const proofs = isRecord(proof.proofs) ? proof.proofs : {};
93
+ const recall = isRecord(proof.recall) ? proof.recall : {};
94
+ const claims = isRecord(proof.claims) ? proof.claims : {};
95
+ const durableItems = numberValue(proofs, ["durable_items", "durableItems"]);
96
+ return {
97
+ status: textValue(proof, ["status"], "quiet"),
98
+ modelContinuity: {
99
+ activeModel: textValue(modelContinuity, ["active_model", "activeModel"], fallbackModelName),
100
+ brainOwner: textValue(modelContinuity, ["brain_owner", "brainOwner"], "lattice_brain"),
101
+ capability: booleanValue(modelContinuity, ["capability"], true),
102
+ survivesModelSwitch: booleanValue(modelContinuity, ["survives_model_switch", "survivesModelSwitch"], false),
103
+ proven: booleanValue(modelContinuity, ["proven"], false),
104
+ contextStore: textValue(modelContinuity, ["context_store", "contextStore"], "workspace + conversation + graph + vector"),
105
+ },
106
+ proofs: {
107
+ durableItems,
108
+ hasDurableEvidence: booleanValue(proofs, ["has_durable_evidence", "hasDurableEvidence"], durableItems > 0),
109
+ workspaceMemories: numberValue(proofs, ["workspace_memories", "workspaceMemories"]),
110
+ conversations: numberValue(proofs, ["conversations"]),
111
+ graphConcepts: numberValue(proofs, ["graph_concepts", "graphConcepts"]),
112
+ vectorItems: numberValue(proofs, ["vector_items", "vectorItems"]),
113
+ healthySources: numberValue(proofs, ["healthy_sources", "healthySources"]),
114
+ },
115
+ recall: {
116
+ query: textValue(recall, ["query"]),
117
+ count: numberValue(recall, ["count"]),
118
+ items: asArray<ApiRecord>(recall.items).map((item, index) => ({
119
+ id: textValue(item, ["id"], `recall-${index}`),
120
+ source: titleValue(item, ["source"], "Memory"),
121
+ title: textValue(item, ["title"], "Memory"),
122
+ snippet: textValue(item, ["snippet"]),
123
+ score: numberValue(item, ["score"]),
124
+ })),
125
+ },
126
+ claims: {
127
+ canRecallUserContext: booleanValue(claims, ["can_recall_user_context", "canRecallUserContext"], false),
128
+ keepsContextAcrossModels: booleanValue(claims, ["keeps_context_across_models", "keepsContextAcrossModels"], false),
129
+ isKnowledgeStore: booleanValue(claims, ["is_knowledge_store", "isKnowledgeStore"], false),
130
+ },
131
+ };
132
+ }
133
+
63
134
  function uniqueById<T extends { id: string }>(items: T[]) {
64
135
  const seen = new Set<string>();
65
136
  return items.filter((item) => {
@@ -69,6 +140,51 @@ function uniqueById<T extends { id: string }>(items: T[]) {
69
140
  });
70
141
  }
71
142
 
143
+ function fallbackBrainReadiness(memoryCount: number, conceptCount: number): BrainReadiness {
144
+ const score = Math.min(100, Math.round(memoryCount * 12 + conceptCount * 10));
145
+ if (memoryCount < 1) {
146
+ return {
147
+ score: Math.max(12, score),
148
+ state: "quiet",
149
+ depth: 2,
150
+ titleKey: "brain.readiness.quiet",
151
+ actionKey: "brain.readiness.start",
152
+ source: "frontend_fallback",
153
+ signals: { memoryCount, conceptCount, relationshipCount: 0, healthySources: 0 },
154
+ };
155
+ }
156
+ if (conceptCount < 3) {
157
+ return {
158
+ score: Math.max(38, score),
159
+ state: "forming",
160
+ depth: 3,
161
+ titleKey: "brain.readiness.forming",
162
+ actionKey: "brain.readiness.grow",
163
+ source: "frontend_fallback",
164
+ signals: { memoryCount, conceptCount, relationshipCount: 0, healthySources: 0 },
165
+ };
166
+ }
167
+ return {
168
+ score: Math.max(72, score),
169
+ state: "alive",
170
+ depth: 5,
171
+ titleKey: "brain.readiness.alive",
172
+ actionKey: "brain.readiness.map",
173
+ source: "frontend_fallback",
174
+ signals: { memoryCount, conceptCount, relationshipCount: 0, healthySources: 0 },
175
+ };
176
+ }
177
+
178
+ function readinessActionKey(state: "quiet" | "forming" | "alive") {
179
+ if (state === "quiet") return "brain.readiness.start";
180
+ if (state === "forming") return "brain.readiness.grow";
181
+ return "brain.readiness.map";
182
+ }
183
+
184
+ function isBrainDepth(value: number): value is BrainDepth {
185
+ return value >= 1 && value <= 5 && Number.isInteger(value);
186
+ }
187
+
72
188
  export function isRecord(value: unknown): value is ApiRecord {
73
189
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
74
190
  }
@@ -96,3 +212,11 @@ function numberValue(record: ApiRecord, keys: string[]) {
96
212
  }
97
213
  return 0;
98
214
  }
215
+
216
+ function booleanValue(record: ApiRecord, keys: string[], fallback: boolean) {
217
+ for (const key of keys) {
218
+ const value = record[key];
219
+ if (typeof value === "boolean") return value;
220
+ }
221
+ return fallback;
222
+ }
@@ -35,6 +35,58 @@ export type KnowledgeGraphModel = {
35
35
  edges: RelationshipThread[];
36
36
  };
37
37
 
38
+ export type BrainReadiness = {
39
+ score: number;
40
+ state: "quiet" | "forming" | "alive";
41
+ depth: BrainDepth;
42
+ titleKey: string;
43
+ actionKey: string;
44
+ source: "memory_service" | "frontend_fallback";
45
+ signals: {
46
+ memoryCount: number;
47
+ conceptCount: number;
48
+ relationshipCount: number;
49
+ healthySources: number;
50
+ };
51
+ };
52
+
53
+ export type BrainProof = {
54
+ status: "quiet" | "forming" | "alive" | string;
55
+ modelContinuity: {
56
+ activeModel: string;
57
+ brainOwner: string;
58
+ capability: boolean;
59
+ survivesModelSwitch: boolean;
60
+ proven: boolean;
61
+ contextStore: string;
62
+ };
63
+ proofs: {
64
+ durableItems: number;
65
+ hasDurableEvidence: boolean;
66
+ workspaceMemories: number;
67
+ conversations: number;
68
+ graphConcepts: number;
69
+ vectorItems: number;
70
+ healthySources: number;
71
+ };
72
+ recall: {
73
+ query: string;
74
+ count: number;
75
+ items: Array<{
76
+ id: string;
77
+ source: string;
78
+ title: string;
79
+ snippet: string;
80
+ score: number;
81
+ }>;
82
+ };
83
+ claims: {
84
+ canRecallUserContext: boolean;
85
+ keepsContextAcrossModels: boolean;
86
+ isKnowledgeStore: boolean;
87
+ };
88
+ };
89
+
38
90
  export const DEPTHS: Array<{ level: BrainDepth; labelKey: string; state: BrainState }> = [
39
91
  { level: 1, labelKey: "brain.depthLabel.1", state: "idle" },
40
92
  { level: 2, labelKey: "brain.depthLabel.2", state: "recalling" },
@@ -31,6 +31,7 @@ export const COPY: Record<Language, TextMap> = {
31
31
  "brain.aria.home": "Lattice Brain",
32
32
  "brain.aria.exploration": "Brain 탐색",
33
33
  "brain.aria.quickViews": "Brain 빠른 보기",
34
+ "brain.depthRail.aria": "Brain 깊이 진행 상태",
34
35
  "brain.aria.conversation": "대화",
35
36
  "brain.aria.ownership": "Brain 소유 보장",
36
37
  "brain.aria.starterPrompts": "시작 프롬프트",
@@ -52,11 +53,19 @@ export const COPY: Record<Language, TextMap> = {
52
53
  "brain.prompt.know": "이 문서를 내 Brain에 넣고 요약해줘: ",
53
54
  "brain.prompt.plan": "지난 결정들을 나중에 찾을 수 있게 정리해줘: ",
54
55
  "brain.placeholder": "Brain에게 말하기...",
56
+ "brain.upload.cta": "파일/문서 넣기",
57
+ "brain.upload.ctaShort": "문서",
58
+ "brain.upload.uploading": "넣는 중",
59
+ "brain.upload.hint": "업로드하면 실제 기억과 그래프로 다시 보입니다.",
60
+ "brain.upload.pending": "{name}을 Brain에 넣는 중입니다.",
61
+ "brain.upload.saved": "{name}이 Brain에 저장됐습니다.",
62
+ "brain.upload.failed": "문서를 넣지 못했습니다: {reason}",
55
63
  "brain.image": "이미지",
56
64
  "brain.unavailable": "지금은 답할 수 없음",
57
65
  "brain.imageAttached": "이미지 첨부됨",
58
66
  "brain.send": "보내기",
59
67
  "brain.saved": "기억에 저장됨 · 연결된 주제 {topics}개 · 관련 기억 {memories}개",
68
+ "brain.saved.detail": "출처와 함께 나중에 다시 불러올 수 있습니다.",
60
69
  "brain.recalled": "기억을 다시 꺼냈습니다: {title}",
61
70
  "brain.overview.kicker": "Brain 한눈에 보기",
62
71
  "brain.overview.title": "기억과 주제를 바로 확인하세요.",
@@ -67,6 +76,30 @@ export const COPY: Record<Language, TextMap> = {
67
76
  "brain.overview.olderEmpty": "대화가 쌓이면 과거 기억이 보입니다.",
68
77
  "brain.overview.topics": "주요 주제",
69
78
  "brain.overview.topicsEmpty": "주제가 형성되는 중입니다.",
79
+ "brain.readiness.kicker": "Brain 준비도",
80
+ "brain.readiness.aria": "Brain 준비도 점수",
81
+ "brain.readiness.quiet": "첫 기억을 기다리고 있습니다.",
82
+ "brain.readiness.forming": "기억이 주제로 자라는 중입니다.",
83
+ "brain.readiness.alive": "기억과 관계가 연결되어 있습니다.",
84
+ "brain.readiness.start": "첫 기억 보기",
85
+ "brain.readiness.grow": "주제 키우기",
86
+ "brain.readiness.map": "지도 보기",
87
+ "brain.proof.aria": "Brain 증거",
88
+ "brain.proof.context": "다시 불러올 맥락",
89
+ "brain.proof.contextValue": "{count}개 저장됨",
90
+ "brain.proof.contextEmpty": "첫 기억 대기",
91
+ "brain.proof.model": "모델 교체 후에도 유지",
92
+ "brain.proof.noModel": "모델 미로드",
93
+ "brain.proof.modelPending": "저장 후 증명됨",
94
+ "brain.proof.store": "지식 저장소",
95
+ "brain.proof.storeValue": "주제 {topics}개 · 벡터 {vectors}개",
96
+ "brain.proof.recall": "방금 Brain이 다시 꺼낸 기억",
97
+ "brain.proof.recallEmpty.kicker": "Brain 증거 자리",
98
+ "brain.proof.recallEmpty.title": "첫 대화 후 여기에 다시 꺼낸 기억이 표시됩니다.",
99
+ "brain.proof.recallEmpty.detail": "아직 저장된 기억이 없어서 가짜 예시는 보여주지 않습니다.",
100
+ "brain.proof.recallPending": "기억은 저장됐고 recall 증거를 준비 중입니다.",
101
+ "brain.proof.recallPending.detail": "인덱싱이 끝나면 이 자리에서 실제 기억을 다시 보여줍니다.",
102
+ "brain.memory.empty.kicker": "첫 기억",
70
103
  "brain.memory.empty": "기억이 조용합니다.",
71
104
  "brain.knowledge.empty": "지식이 형성되는 중입니다.",
72
105
  "brain.graph.empty": "아직 맞는 지식이 없습니다.",
@@ -352,6 +385,7 @@ export const COPY: Record<Language, TextMap> = {
352
385
  "brain.aria.home": "Lattice Brain",
353
386
  "brain.aria.exploration": "Brain exploration",
354
387
  "brain.aria.quickViews": "Brain quick views",
388
+ "brain.depthRail.aria": "Brain depth progress",
355
389
  "brain.aria.conversation": "Conversation",
356
390
  "brain.aria.ownership": "Brain ownership guarantees",
357
391
  "brain.aria.starterPrompts": "Starter prompts",
@@ -373,11 +407,19 @@ export const COPY: Record<Language, TextMap> = {
373
407
  "brain.prompt.know": "Put this document into my Brain and summarize it: ",
374
408
  "brain.prompt.plan": "Organize past decisions so I can find them later: ",
375
409
  "brain.placeholder": "Talk to your Brain...",
410
+ "brain.upload.cta": "Add file or document",
411
+ "brain.upload.ctaShort": "Document",
412
+ "brain.upload.uploading": "Adding...",
413
+ "brain.upload.hint": "After upload, it reappears as real memory and graph context.",
414
+ "brain.upload.pending": "Adding {name} to Brain.",
415
+ "brain.upload.saved": "{name} was saved to Brain.",
416
+ "brain.upload.failed": "Could not add the document: {reason}",
376
417
  "brain.image": "Image",
377
418
  "brain.unavailable": "Unavailable",
378
419
  "brain.imageAttached": "Image attached",
379
420
  "brain.send": "Send",
380
421
  "brain.saved": "Saved to memory · {topics} linked topics · {memories} related memories",
422
+ "brain.saved.detail": "Available later with source-aware recall.",
381
423
  "brain.recalled": "Recalled this memory: {title}",
382
424
  "brain.overview.kicker": "Brain at a glance",
383
425
  "brain.overview.title": "See memories and topics immediately.",
@@ -388,6 +430,30 @@ export const COPY: Record<Language, TextMap> = {
388
430
  "brain.overview.olderEmpty": "Earlier memories appear as conversations accumulate.",
389
431
  "brain.overview.topics": "Main topics",
390
432
  "brain.overview.topicsEmpty": "Topics are still forming.",
433
+ "brain.readiness.kicker": "Brain readiness",
434
+ "brain.readiness.aria": "Brain readiness score",
435
+ "brain.readiness.quiet": "Waiting for the first memory.",
436
+ "brain.readiness.forming": "Memories are becoming topics.",
437
+ "brain.readiness.alive": "Memories and relationships are connected.",
438
+ "brain.readiness.start": "View first memory",
439
+ "brain.readiness.grow": "Grow topics",
440
+ "brain.readiness.map": "Open map",
441
+ "brain.proof.aria": "Brain proof",
442
+ "brain.proof.context": "Recallable context",
443
+ "brain.proof.contextValue": "{count} saved",
444
+ "brain.proof.contextEmpty": "Waiting for first memory",
445
+ "brain.proof.model": "Survives model switches",
446
+ "brain.proof.noModel": "No model loaded",
447
+ "brain.proof.modelPending": "Proven after saving",
448
+ "brain.proof.store": "Knowledge store",
449
+ "brain.proof.storeValue": "{topics} topics · {vectors} vectors",
450
+ "brain.proof.recall": "Just recalled by Brain",
451
+ "brain.proof.recallEmpty.kicker": "Brain proof slot",
452
+ "brain.proof.recallEmpty.title": "After your first exchange, recalled memory appears here.",
453
+ "brain.proof.recallEmpty.detail": "No saved memory yet, so Lattice does not show a fake example.",
454
+ "brain.proof.recallPending": "Memory is saved; recall proof is preparing.",
455
+ "brain.proof.recallPending.detail": "When indexing finishes, this slot shows the actual memory Brain can bring back.",
456
+ "brain.memory.empty.kicker": "First memory",
391
457
  "brain.memory.empty": "Memory is quiet",
392
458
  "brain.knowledge.empty": "Knowledge is forming",
393
459
  "brain.graph.empty": "No matching knowledge yet",