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.
- package/README.md +29 -31
- package/docs/CHANGELOG.md +48 -0
- package/frontend/src/App.tsx +80 -0
- package/frontend/src/api/client.ts +11 -0
- package/frontend/src/components/AdminAccessGate.tsx +70 -0
- package/frontend/src/components/BrainConversation.tsx +232 -8
- package/frontend/src/components/FeedbackState.tsx +45 -0
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
- package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
- package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
- package/frontend/src/features/brain/BrainConversation.tsx +382 -7
- package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
- package/frontend/src/features/brain/BrainHome.tsx +300 -1
- package/frontend/src/features/brain/brainData.ts +25 -1
- package/frontend/src/features/brain/graphLayout.ts +12 -1
- package/frontend/src/features/brain/types.ts +54 -0
- package/frontend/src/i18n.ts +374 -0
- package/frontend/src/store/appStore.ts +15 -0
- package/frontend/src/styles.css +1383 -41
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/workspace.py +59 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +1 -1
- package/scripts/brain_quality_eval.py +118 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +28 -28
- package/static/app/assets/Act-BHEb8bAM.js +2 -0
- package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
- package/static/app/assets/Brain-yvR7xkrV.js +322 -0
- package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
- package/static/app/assets/Capture-BrAmsSEH.js +2 -0
- package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
- package/static/app/assets/Library-BOVzYfxI.js +2 -0
- package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
- package/static/app/assets/System-D4WaN4kj.js +2 -0
- package/static/app/assets/System-D4WaN4kj.js.map +1 -0
- package/static/app/assets/index-BOB-W1FZ.js +17 -0
- package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
- package/static/app/assets/index-DvLsGy-z.css +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js +2 -0
- package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
- package/static/app/assets/textarea-CbvhHvzg.js +2 -0
- package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
- package/static/app/index.html +2 -2
- package/static/app/assets/Act-DGN37eR4.js +0 -2
- package/static/app/assets/Brain-HDcIwPHW.js +0 -322
- package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
- package/static/app/assets/Capture--eZfb_Ex.js +0 -2
- package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
- package/static/app/assets/Library-BbagRFyd.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js +0 -2
- package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
- package/static/app/assets/index-C950aES_.js +0 -17
- package/static/app/assets/index-C950aES_.js.map +0 -1
- package/static/app/assets/index-ztppMg0c.css +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
- package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
- package/static/app/assets/textarea-C2PLu6_O.js +0 -2
|
@@ -7,7 +7,19 @@ import { t } from "@/i18n";
|
|
|
7
7
|
import { BrainConversation } from "./BrainConversation";
|
|
8
8
|
import { buildBrainProof, buildBrainReadiness, buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
|
|
9
9
|
import { DepthEmergence } from "./DepthEmergence";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DEPTHS,
|
|
12
|
+
INGESTION_STAGE_ORDER,
|
|
13
|
+
type BrainDepth,
|
|
14
|
+
type BrainProof,
|
|
15
|
+
type EmergenceEvent,
|
|
16
|
+
type IngestionPipelineStage,
|
|
17
|
+
type IngestionSourceType,
|
|
18
|
+
type IngestionState,
|
|
19
|
+
type MemoryFragment,
|
|
20
|
+
type Message,
|
|
21
|
+
type MessageProof,
|
|
22
|
+
} from "./types";
|
|
11
23
|
|
|
12
24
|
export function BrainHome({
|
|
13
25
|
brainState,
|
|
@@ -31,8 +43,27 @@ export function BrainHome({
|
|
|
31
43
|
const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
|
|
32
44
|
const [uploadingDocument, setUploadingDocument] = React.useState(false);
|
|
33
45
|
const [lastRecallQuery, setLastRecallQuery] = React.useState("");
|
|
46
|
+
const [ingestionStates, setIngestionStates] = React.useState<Record<IngestionSourceType, IngestionState | null>>({
|
|
47
|
+
file: null,
|
|
48
|
+
folder: null,
|
|
49
|
+
note: null,
|
|
50
|
+
web: null,
|
|
51
|
+
});
|
|
52
|
+
const [emergenceEvents, setEmergenceEvents] = React.useState<EmergenceEvent[]>([]);
|
|
34
53
|
const streamRef = React.useRef<HTMLDivElement>(null);
|
|
35
54
|
const recallTimerRef = React.useRef<number | null>(null);
|
|
55
|
+
const stageTimersRef = React.useRef<Record<IngestionSourceType, number[]>>({
|
|
56
|
+
file: [],
|
|
57
|
+
folder: [],
|
|
58
|
+
note: [],
|
|
59
|
+
web: [],
|
|
60
|
+
});
|
|
61
|
+
const pendingBaselineRef = React.useRef<
|
|
62
|
+
Partial<Record<IngestionSourceType, { memories: number; entities: number; label: string }>>
|
|
63
|
+
>({});
|
|
64
|
+
// Source types whose request has resolved and are awaiting count settle to record emergence.
|
|
65
|
+
const awaitingEmergenceRef = React.useRef<Set<IngestionSourceType>>(new Set());
|
|
66
|
+
const settleTimerRef = React.useRef<number | null>(null);
|
|
36
67
|
|
|
37
68
|
const memoriesQ = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
|
|
38
69
|
const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
|
|
@@ -89,9 +120,156 @@ export function BrainHome({
|
|
|
89
120
|
React.useEffect(() => {
|
|
90
121
|
return () => {
|
|
91
122
|
if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
|
|
123
|
+
for (const timers of Object.values(stageTimersRef.current)) {
|
|
124
|
+
for (const timer of timers) window.clearTimeout(timer);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const clearStageTimers = React.useCallback((sourceType: IngestionSourceType) => {
|
|
130
|
+
for (const timer of stageTimersRef.current[sourceType]) window.clearTimeout(timer);
|
|
131
|
+
stageTimersRef.current[sourceType] = [];
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const setStage = React.useCallback((sourceType: IngestionSourceType, stage: IngestionPipelineStage) => {
|
|
135
|
+
setIngestionStates((prev) => {
|
|
136
|
+
const current = prev[sourceType];
|
|
137
|
+
if (!current) return prev;
|
|
138
|
+
return {
|
|
139
|
+
...prev,
|
|
140
|
+
[sourceType]: {
|
|
141
|
+
...current,
|
|
142
|
+
stage,
|
|
143
|
+
completedAt: stage === "complete" || stage === "error" ? Date.now() : current.completedAt,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
const beginIngestion = React.useCallback(
|
|
150
|
+
(sourceType: IngestionSourceType, label: string) => {
|
|
151
|
+
clearStageTimers(sourceType);
|
|
152
|
+
pendingBaselineRef.current[sourceType] = {
|
|
153
|
+
memories: memoryFragments.length,
|
|
154
|
+
entities: knowledgeConcepts.length,
|
|
155
|
+
label,
|
|
156
|
+
};
|
|
157
|
+
setIngestionStates((prev) => ({
|
|
158
|
+
...prev,
|
|
159
|
+
[sourceType]: {
|
|
160
|
+
sourceType,
|
|
161
|
+
label,
|
|
162
|
+
stage: "preparing",
|
|
163
|
+
startedAt: Date.now(),
|
|
164
|
+
completedAt: null,
|
|
165
|
+
newMemories: 0,
|
|
166
|
+
newEntities: 0,
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
// Progressive disclosure of the in-flight pipeline while the request runs.
|
|
170
|
+
const interim: IngestionPipelineStage[] = ["parsing", "embedding", "indexing"];
|
|
171
|
+
interim.forEach((stage, index) => {
|
|
172
|
+
const timer = window.setTimeout(() => setStage(sourceType, stage), 420 * (index + 1));
|
|
173
|
+
stageTimersRef.current[sourceType].push(timer);
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
[clearStageTimers, knowledgeConcepts.length, memoryFragments.length, setStage],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const resolveEmergence = React.useCallback(
|
|
180
|
+
(sourceType: IngestionSourceType, memoryCount: number, entityCount: number) => {
|
|
181
|
+
clearStageTimers(sourceType);
|
|
182
|
+
const baseline = pendingBaselineRef.current[sourceType];
|
|
183
|
+
const label = baseline?.label ?? "";
|
|
184
|
+
// Snapshot deltas after invalidation lands; cap at >=0 to avoid noise.
|
|
185
|
+
const newMemories = Math.max(0, memoryCount - (baseline?.memories ?? memoryCount));
|
|
186
|
+
const newEntities = Math.max(0, entityCount - (baseline?.entities ?? entityCount));
|
|
187
|
+
delete pendingBaselineRef.current[sourceType];
|
|
188
|
+
setIngestionStates((prev) => {
|
|
189
|
+
const current = prev[sourceType];
|
|
190
|
+
if (!current) return prev;
|
|
191
|
+
return {
|
|
192
|
+
...prev,
|
|
193
|
+
[sourceType]: {
|
|
194
|
+
...current,
|
|
195
|
+
stage: "complete",
|
|
196
|
+
completedAt: Date.now(),
|
|
197
|
+
newMemories,
|
|
198
|
+
newEntities,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
setEmergenceEvents((events) =>
|
|
203
|
+
[
|
|
204
|
+
{
|
|
205
|
+
id: `${sourceType}-${Date.now()}`,
|
|
206
|
+
sourceType,
|
|
207
|
+
label,
|
|
208
|
+
newMemories,
|
|
209
|
+
newEntities,
|
|
210
|
+
at: Date.now(),
|
|
211
|
+
},
|
|
212
|
+
...events,
|
|
213
|
+
].slice(0, 10),
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
[clearStageTimers],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Once a request resolves we wait for the refetched counts to settle, then record the
|
|
220
|
+
// real emergence delta. A fallback timer guarantees the panel never hangs on a stale count.
|
|
221
|
+
const markAwaitingEmergence = React.useCallback(
|
|
222
|
+
(sourceType: IngestionSourceType) => {
|
|
223
|
+
awaitingEmergenceRef.current.add(sourceType);
|
|
224
|
+
if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
|
|
225
|
+
settleTimerRef.current = window.setTimeout(() => {
|
|
226
|
+
for (const pending of Array.from(awaitingEmergenceRef.current)) {
|
|
227
|
+
resolveEmergence(pending, memoryFragments.length, knowledgeConcepts.length);
|
|
228
|
+
awaitingEmergenceRef.current.delete(pending);
|
|
229
|
+
}
|
|
230
|
+
}, 1600);
|
|
231
|
+
},
|
|
232
|
+
[knowledgeConcepts.length, memoryFragments.length, resolveEmergence],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// Flush awaiting ingestions as soon as the underlying counts change post-invalidation.
|
|
236
|
+
React.useEffect(() => {
|
|
237
|
+
if (awaitingEmergenceRef.current.size === 0) return;
|
|
238
|
+
for (const pending of Array.from(awaitingEmergenceRef.current)) {
|
|
239
|
+
const baseline = pendingBaselineRef.current[pending];
|
|
240
|
+
if (!baseline) {
|
|
241
|
+
awaitingEmergenceRef.current.delete(pending);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (memoryFragments.length !== baseline.memories || knowledgeConcepts.length !== baseline.entities) {
|
|
245
|
+
resolveEmergence(pending, memoryFragments.length, knowledgeConcepts.length);
|
|
246
|
+
awaitingEmergenceRef.current.delete(pending);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}, [memoryFragments.length, knowledgeConcepts.length, resolveEmergence]);
|
|
250
|
+
|
|
251
|
+
React.useEffect(() => {
|
|
252
|
+
return () => {
|
|
253
|
+
if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
|
|
92
254
|
};
|
|
93
255
|
}, []);
|
|
94
256
|
|
|
257
|
+
const failIngestion = React.useCallback(
|
|
258
|
+
(sourceType: IngestionSourceType, reason: string) => {
|
|
259
|
+
clearStageTimers(sourceType);
|
|
260
|
+
delete pendingBaselineRef.current[sourceType];
|
|
261
|
+
setIngestionStates((prev) => {
|
|
262
|
+
const current = prev[sourceType];
|
|
263
|
+
if (!current) return prev;
|
|
264
|
+
return {
|
|
265
|
+
...prev,
|
|
266
|
+
[sourceType]: { ...current, stage: "error", completedAt: Date.now(), error: reason },
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
[clearStageTimers],
|
|
271
|
+
);
|
|
272
|
+
|
|
95
273
|
async function send() {
|
|
96
274
|
const text = draft.trim();
|
|
97
275
|
if (!text || streaming) return;
|
|
@@ -134,6 +312,7 @@ export function BrainHome({
|
|
|
134
312
|
} else {
|
|
135
313
|
setMemoryFeedback(t(language, "brain.saved", { topics: knowledgeConcepts.length, memories: memoryFragments.length }));
|
|
136
314
|
setLastRecallQuery(text);
|
|
315
|
+
void attachAnswerProof(text);
|
|
137
316
|
}
|
|
138
317
|
} finally {
|
|
139
318
|
setStreaming(false);
|
|
@@ -144,17 +323,35 @@ export function BrainHome({
|
|
|
144
323
|
}
|
|
145
324
|
}
|
|
146
325
|
|
|
326
|
+
async function attachAnswerProof(query: string) {
|
|
327
|
+
const proofResult = await latticeApi.memoryBrainProof(query, 4);
|
|
328
|
+
const proof = buildBrainProof(proofResult.data, modelName);
|
|
329
|
+
const messageProof = toMessageProof(proof, query, modelName);
|
|
330
|
+
setMessages((items) => {
|
|
331
|
+
const next = [...items];
|
|
332
|
+
for (let index = next.length - 1; index >= 0; index -= 1) {
|
|
333
|
+
if (next[index].role === "assistant") {
|
|
334
|
+
next[index] = { ...next[index], proof: messageProof };
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return next;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
147
342
|
async function uploadDocument(file: File) {
|
|
148
343
|
if (uploadingDocument) return;
|
|
149
344
|
|
|
150
345
|
setUploadingDocument(true);
|
|
151
346
|
setMemoryFeedback(t(language, "brain.upload.pending", { name: file.name }));
|
|
152
347
|
onBrainChange("recalling", 0.86);
|
|
348
|
+
beginIngestion("file", file.name);
|
|
153
349
|
|
|
154
350
|
try {
|
|
155
351
|
const result = await latticeApi.uploadDocument(file);
|
|
156
352
|
if (result.error) {
|
|
157
353
|
setMemoryFeedback(t(language, "brain.upload.failed", { reason: result.error }));
|
|
354
|
+
failIngestion("file", result.error);
|
|
158
355
|
return;
|
|
159
356
|
}
|
|
160
357
|
|
|
@@ -164,11 +361,93 @@ export function BrainHome({
|
|
|
164
361
|
void qc.invalidateQueries({ queryKey: ["memoryManager"] });
|
|
165
362
|
void qc.invalidateQueries({ queryKey: ["graph"] });
|
|
166
363
|
void qc.invalidateQueries({ queryKey: ["memoryBrainProof"] });
|
|
364
|
+
markAwaitingEmergence("file");
|
|
167
365
|
} finally {
|
|
168
366
|
setUploadingDocument(false);
|
|
169
367
|
}
|
|
170
368
|
}
|
|
171
369
|
|
|
370
|
+
async function connectFolder(path: string) {
|
|
371
|
+
const target = path.trim();
|
|
372
|
+
if (!target) return;
|
|
373
|
+
setMemoryFeedback(t(language, "brain.ingest.folder.pending", { path: target }));
|
|
374
|
+
onBrainChange("recalling", 0.84);
|
|
375
|
+
beginIngestion("folder", target);
|
|
376
|
+
const result = await latticeApi.connectFolder(target);
|
|
377
|
+
if (result.error || !result.ok) {
|
|
378
|
+
setMemoryFeedback(t(language, "brain.ingest.folder.failed", { reason: result.error || "unavailable" }));
|
|
379
|
+
failIngestion("folder", result.error || "unavailable");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
setMemoryFeedback(t(language, "brain.ingest.folder.saved", { path: target }));
|
|
383
|
+
setLastRecallQuery(target);
|
|
384
|
+
triggerBrainRecall();
|
|
385
|
+
void refreshBrainProof(target);
|
|
386
|
+
markAwaitingEmergence("folder");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function ingestNote(note: string) {
|
|
390
|
+
const content = note.trim();
|
|
391
|
+
if (!content) return;
|
|
392
|
+
setMemoryFeedback(t(language, "brain.ingest.note.pending"));
|
|
393
|
+
onBrainChange("recalling", 0.84);
|
|
394
|
+
beginIngestion("note", content.slice(0, 80));
|
|
395
|
+
const result = await latticeApi.ingestNote(content, content.slice(0, 80));
|
|
396
|
+
if (result.error || !result.ok) {
|
|
397
|
+
setMemoryFeedback(t(language, "brain.ingest.note.failed", { reason: result.error || "unavailable" }));
|
|
398
|
+
failIngestion("note", result.error || "unavailable");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
setMemoryFeedback(t(language, "brain.ingest.note.saved"));
|
|
402
|
+
setLastRecallQuery(content.slice(0, 120));
|
|
403
|
+
triggerBrainRecall();
|
|
404
|
+
void refreshBrainProof(content.slice(0, 120));
|
|
405
|
+
markAwaitingEmergence("note");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function ingestWeb(url: string) {
|
|
409
|
+
const target = url.trim();
|
|
410
|
+
if (!target) return;
|
|
411
|
+
setMemoryFeedback(t(language, "brain.ingest.web.pending", { url: target }));
|
|
412
|
+
onBrainChange("recalling", 0.84);
|
|
413
|
+
beginIngestion("web", target);
|
|
414
|
+
const result = await latticeApi.browserReadUrl(target);
|
|
415
|
+
if (result.error || !result.ok) {
|
|
416
|
+
setMemoryFeedback(t(language, "brain.ingest.web.failed", { reason: result.error || "unavailable" }));
|
|
417
|
+
failIngestion("web", result.error || "unavailable");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
setMemoryFeedback(t(language, "brain.ingest.web.saved", { url: target }));
|
|
421
|
+
setLastRecallQuery(target);
|
|
422
|
+
triggerBrainRecall();
|
|
423
|
+
void refreshBrainProof(target);
|
|
424
|
+
markAwaitingEmergence("web");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function refreshBrainProof(query = lastRecallQuery) {
|
|
428
|
+
await Promise.all([
|
|
429
|
+
qc.invalidateQueries({ queryKey: ["memoryManager"] }),
|
|
430
|
+
qc.invalidateQueries({ queryKey: ["graph"] }),
|
|
431
|
+
qc.invalidateQueries({ queryKey: ["memoryBrainProof"] }),
|
|
432
|
+
]);
|
|
433
|
+
if (query.trim()) {
|
|
434
|
+
await attachAnswerProof(query);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function verifyModelContinuity() {
|
|
439
|
+
const lastUserMessage = [...messages].reverse().find((message: Message) => message.role === "user");
|
|
440
|
+
const query = lastRecallQuery || lastUserMessage?.content || "";
|
|
441
|
+
if (!query.trim()) {
|
|
442
|
+
setMemoryFeedback(t(language, "brain.modelDemo.needQuestion"));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
setMemoryFeedback(t(language, "brain.modelDemo.checking", { model: modelName }));
|
|
446
|
+
await attachAnswerProof(query);
|
|
447
|
+
setLastRecallQuery(query);
|
|
448
|
+
setMemoryFeedback(t(language, "brain.modelDemo.done", { model: modelName }));
|
|
449
|
+
}
|
|
450
|
+
|
|
172
451
|
function deepen() {
|
|
173
452
|
setExplorationDepth((depth) => {
|
|
174
453
|
const next = Math.min(5, depth + 1) as BrainDepth;
|
|
@@ -272,6 +551,8 @@ export function BrainHome({
|
|
|
272
551
|
messages={messages}
|
|
273
552
|
starterPrompts={starterPrompts}
|
|
274
553
|
memoryFeedback={memoryFeedback}
|
|
554
|
+
ingestionStates={ingestionStates}
|
|
555
|
+
emergenceEvents={emergenceEvents}
|
|
275
556
|
draft={draft}
|
|
276
557
|
streaming={streaming}
|
|
277
558
|
imageData={imageData}
|
|
@@ -285,8 +566,26 @@ export function BrainHome({
|
|
|
285
566
|
onDraftChange={setDraft}
|
|
286
567
|
onImageDataChange={setImageData}
|
|
287
568
|
onUploadDocument={(file) => void uploadDocument(file)}
|
|
569
|
+
onConnectFolder={(path) => void connectFolder(path)}
|
|
570
|
+
onIngestNote={(note) => void ingestNote(note)}
|
|
571
|
+
onIngestWeb={(url) => void ingestWeb(url)}
|
|
572
|
+
onVerifyModelContinuity={() => void verifyModelContinuity()}
|
|
288
573
|
onSend={() => void send()}
|
|
289
574
|
/>
|
|
290
575
|
</main>
|
|
291
576
|
);
|
|
292
577
|
}
|
|
578
|
+
|
|
579
|
+
function toMessageProof(proof: BrainProof, query: string, fallbackModelName: string): MessageProof {
|
|
580
|
+
return {
|
|
581
|
+
query,
|
|
582
|
+
model: proof.modelContinuity.activeModel || fallbackModelName,
|
|
583
|
+
provenAcrossModels: proof.modelContinuity.proven && proof.claims.keepsContextAcrossModels,
|
|
584
|
+
citations: proof.recall.items.slice(0, 4).map((item) => ({
|
|
585
|
+
id: item.id,
|
|
586
|
+
source: item.source,
|
|
587
|
+
title: item.title,
|
|
588
|
+
snippet: item.snippet,
|
|
589
|
+
})),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
@@ -33,7 +33,9 @@ export function parseKnowledgeGraph(data: unknown): KnowledgeGraphModel {
|
|
|
33
33
|
const label = textValue(node, ["title", "label", "name"], id.replace(/^[^:]+:/, ""));
|
|
34
34
|
const summary = textValue(node, ["summary", "description", "snippet"]) || textValue(metadata, ["summary", "description", "relative_path", "filename"]);
|
|
35
35
|
const importance = clamp(numberValue(node, ["importance_norm", "importance", "score"]) || 0.5, 0.08, 1);
|
|
36
|
-
|
|
36
|
+
const createdAt = timestampValue(node, ["created_at", "createdAt", "added_at", "addedAt", "timestamp", "updated_at", "updatedAt"])
|
|
37
|
+
?? timestampValue(metadata, ["created_at", "createdAt", "added_at", "addedAt", "timestamp", "updated_at", "updatedAt"]);
|
|
38
|
+
return [{ id, label, type, summary, importance, ...(createdAt !== undefined ? { createdAt } : {}) }];
|
|
37
39
|
}).sort((left, right) => right.importance - left.importance);
|
|
38
40
|
const ids = new Set(nodes.map((node) => node.id));
|
|
39
41
|
const edges = rawEdges.flatMap((edge, index): RelationshipThread[] => {
|
|
@@ -205,6 +207,28 @@ function titleValue(record: ApiRecord, keys: string[], fallback = "") {
|
|
|
205
207
|
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
// Parse a created/added timestamp into unix epoch milliseconds. Accepts ISO 8601
|
|
211
|
+
// strings, unix seconds, or unix milliseconds. Returns undefined when absent or
|
|
212
|
+
// unparseable so the time-exploration UI can fall back gracefully.
|
|
213
|
+
function timestampValue(record: ApiRecord, keys: string[]): number | undefined {
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
const value = record[key];
|
|
216
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
217
|
+
// Heuristic: values below ~1e12 are seconds, otherwise milliseconds.
|
|
218
|
+
return value < 1e12 ? value * 1000 : value;
|
|
219
|
+
}
|
|
220
|
+
if (typeof value === "string" && value.trim()) {
|
|
221
|
+
const numeric = Number(value);
|
|
222
|
+
if (Number.isFinite(numeric) && /^\d+$/.test(value.trim())) {
|
|
223
|
+
return numeric < 1e12 ? numeric * 1000 : numeric;
|
|
224
|
+
}
|
|
225
|
+
const parsed = Date.parse(value);
|
|
226
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
208
232
|
function numberValue(record: ApiRecord, keys: string[]) {
|
|
209
233
|
for (const key of keys) {
|
|
210
234
|
const value = Number(record[key]);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import type { KnowledgeConcept } from "./types";
|
|
2
|
+
import type { KnowledgeConcept, RelationshipThread } from "./types";
|
|
3
|
+
|
|
4
|
+
// Return the set of node ids that share a direct (1-hop) edge with `nodeId`.
|
|
5
|
+
// The focused node itself is not included.
|
|
6
|
+
export function computeGraphNeighbors(nodeId: string, edges: RelationshipThread[]): Set<string> {
|
|
7
|
+
const neighbors = new Set<string>();
|
|
8
|
+
for (const edge of edges) {
|
|
9
|
+
if (edge.source === nodeId) neighbors.add(edge.target);
|
|
10
|
+
else if (edge.target === nodeId) neighbors.add(edge.source);
|
|
11
|
+
}
|
|
12
|
+
return neighbors;
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export function layoutGraphNodes(nodes: KnowledgeConcept[], radiusX: number, radiusY: number) {
|
|
5
16
|
return nodes.map((node, index) => {
|
|
@@ -6,6 +6,19 @@ export type BrainDepth = 1 | 2 | 3 | 4 | 5;
|
|
|
6
6
|
export type Message = {
|
|
7
7
|
role: "user" | "assistant";
|
|
8
8
|
content: string;
|
|
9
|
+
proof?: MessageProof;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MessageProof = {
|
|
13
|
+
query: string;
|
|
14
|
+
model: string;
|
|
15
|
+
provenAcrossModels: boolean;
|
|
16
|
+
citations: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
source: string;
|
|
19
|
+
title: string;
|
|
20
|
+
snippet: string;
|
|
21
|
+
}>;
|
|
9
22
|
};
|
|
10
23
|
|
|
11
24
|
export type MemoryFragment = {
|
|
@@ -20,6 +33,9 @@ export type KnowledgeConcept = {
|
|
|
20
33
|
type: string;
|
|
21
34
|
summary: string;
|
|
22
35
|
importance: number;
|
|
36
|
+
// Optional unix epoch (ms) the concept was added to the graph. Enables
|
|
37
|
+
// time-based exploration; absent when the backend does not yet emit it.
|
|
38
|
+
createdAt?: number;
|
|
23
39
|
};
|
|
24
40
|
|
|
25
41
|
export type RelationshipThread = {
|
|
@@ -87,6 +103,44 @@ export type BrainProof = {
|
|
|
87
103
|
};
|
|
88
104
|
};
|
|
89
105
|
|
|
106
|
+
export type IngestionSourceType = "file" | "folder" | "note" | "web";
|
|
107
|
+
|
|
108
|
+
export type IngestionPipelineStage =
|
|
109
|
+
| "preparing"
|
|
110
|
+
| "parsing"
|
|
111
|
+
| "embedding"
|
|
112
|
+
| "indexing"
|
|
113
|
+
| "complete"
|
|
114
|
+
| "error";
|
|
115
|
+
|
|
116
|
+
export type IngestionState = {
|
|
117
|
+
sourceType: IngestionSourceType;
|
|
118
|
+
label: string;
|
|
119
|
+
stage: IngestionPipelineStage;
|
|
120
|
+
startedAt: number;
|
|
121
|
+
completedAt: number | null;
|
|
122
|
+
newMemories: number;
|
|
123
|
+
newEntities: number;
|
|
124
|
+
error?: string;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type EmergenceEvent = {
|
|
128
|
+
id: string;
|
|
129
|
+
sourceType: IngestionSourceType;
|
|
130
|
+
label: string;
|
|
131
|
+
newMemories: number;
|
|
132
|
+
newEntities: number;
|
|
133
|
+
at: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const INGESTION_STAGE_ORDER: IngestionPipelineStage[] = [
|
|
137
|
+
"preparing",
|
|
138
|
+
"parsing",
|
|
139
|
+
"embedding",
|
|
140
|
+
"indexing",
|
|
141
|
+
"complete",
|
|
142
|
+
];
|
|
143
|
+
|
|
90
144
|
export const DEPTHS: Array<{ level: BrainDepth; labelKey: string; state: BrainState }> = [
|
|
91
145
|
{ level: 1, labelKey: "brain.depthLabel.1", state: "idle" },
|
|
92
146
|
{ level: 2, labelKey: "brain.depthLabel.2", state: "recalling" },
|