ltcai 4.5.1 → 4.6.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.
- package/README.md +123 -179
- package/docs/CHANGELOG.md +120 -0
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +72 -0
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +42 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +19 -17
- package/frontend/index.html +2 -2
- package/frontend/src/App.tsx +653 -208
- package/frontend/src/api/client.ts +1 -0
- package/frontend/src/components/BrainConversation.tsx +309 -0
- package/frontend/src/components/FirstRunGuide.tsx +4 -4
- package/frontend/src/components/LivingBrain.tsx +212 -0
- package/frontend/src/components/ProductFlow.tsx +654 -0
- package/frontend/src/pages/Ask.tsx +2 -229
- package/frontend/src/pages/Brain.tsx +68 -49
- package/frontend/src/routes.ts +15 -26
- package/frontend/src/styles.css +2375 -87
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +2 -2
- 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 +5 -5
- package/static/app/assets/index-7U86v70r.css +2 -0
- package/static/app/assets/index-D1jAPQws.js +16 -0
- package/static/app/assets/index-D1jAPQws.js.map +1 -0
- package/static/app/index.html +4 -4
- package/static/manifest.json +1 -1
- package/static/app/assets/index-3G8qcrIS.js +0 -336
- package/static/app/assets/index-3G8qcrIS.js.map +0 -1
- package/static/app/assets/index-C0wYZp7k.css +0 -2
package/frontend/src/App.tsx
CHANGED
|
@@ -1,259 +1,704 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { useQuery } from "@tanstack/react-query";
|
|
3
|
-
import {
|
|
2
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { ImagePlus, Search, Send } from "lucide-react";
|
|
4
4
|
import { latticeApi } from "@/api/client";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { useAppStore
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
|
|
7
|
+
import { ProductFlow, readProductFlowComplete } from "@/components/ProductFlow";
|
|
8
|
+
import { useAppStore } from "@/store/appStore";
|
|
9
|
+
import { asArray } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
type ApiRecord = Record<string, unknown>;
|
|
12
|
+
type BrainDepth = 1 | 2 | 3 | 4 | 5;
|
|
13
|
+
|
|
14
|
+
type Message = {
|
|
15
|
+
role: "user" | "assistant";
|
|
16
|
+
content: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type MemoryFragment = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type KnowledgeConcept = {
|
|
26
|
+
id: string;
|
|
27
|
+
label: string;
|
|
28
|
+
type: string;
|
|
29
|
+
summary: string;
|
|
30
|
+
importance: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type RelationshipThread = {
|
|
34
|
+
id: string;
|
|
35
|
+
source: string;
|
|
36
|
+
target: string;
|
|
37
|
+
label: string;
|
|
38
|
+
weight: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type KnowledgeGraphModel = {
|
|
42
|
+
nodes: KnowledgeConcept[];
|
|
43
|
+
edges: RelationshipThread[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEPTHS: Array<{ level: BrainDepth; label: string; state: BrainState }> = [
|
|
47
|
+
{ level: 1, label: "Living Brain", state: "idle" },
|
|
48
|
+
{ level: 2, label: "Memory Layer", state: "recalling" },
|
|
49
|
+
{ level: 3, label: "Knowledge Layer", state: "synthesizing" },
|
|
50
|
+
{ level: 4, label: "Relationship Layer", state: "planning" },
|
|
51
|
+
{ level: 5, label: "Knowledge Graph", state: "synthesizing" },
|
|
22
52
|
];
|
|
23
53
|
|
|
24
|
-
function
|
|
25
|
-
const
|
|
54
|
+
export default function App() {
|
|
55
|
+
const theme = useAppStore((state) => state.theme);
|
|
56
|
+
const [flowComplete, setFlowComplete] = React.useState(readProductFlowComplete);
|
|
57
|
+
const { state: brainState, intensity, setBrain } = useBrainState();
|
|
58
|
+
|
|
26
59
|
React.useEffect(() => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
60
|
+
document.documentElement.dataset.theme = theme;
|
|
61
|
+
}, [theme]);
|
|
62
|
+
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
const onKey = (event: KeyboardEvent) => {
|
|
65
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
document.querySelector<HTMLTextAreaElement>(".brain-composer textarea")?.focus();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
window.addEventListener("keydown", onKey);
|
|
71
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
30
72
|
}, []);
|
|
31
|
-
return route;
|
|
32
|
-
}
|
|
33
73
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (primary === "act") return <ActPage initialTab={tab} />;
|
|
38
|
-
if (primary === "library") return <LibraryPage initialTab={tab} />;
|
|
39
|
-
if (primary === "system") return <SystemPage initialTab={tab} />;
|
|
40
|
-
return <BrainPage initialTab={tab} />;
|
|
41
|
-
}
|
|
74
|
+
if (!flowComplete) {
|
|
75
|
+
return <ProductFlow onComplete={() => setFlowComplete(true)} />;
|
|
76
|
+
}
|
|
42
77
|
|
|
43
|
-
function AmbientBrain() {
|
|
44
78
|
return (
|
|
45
|
-
<div className="
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<span className="signal-line signal-line-c" />
|
|
49
|
-
<span className="signal-tile signal-tile-a" />
|
|
50
|
-
<span className="signal-tile signal-tile-b" />
|
|
51
|
-
<span className="signal-tile signal-tile-c" />
|
|
79
|
+
<div className="brain-space">
|
|
80
|
+
<div className="brain-field" />
|
|
81
|
+
<BrainHome brainState={brainState} intensity={intensity} onBrainChange={setBrain} />
|
|
52
82
|
</div>
|
|
53
83
|
);
|
|
54
84
|
}
|
|
55
85
|
|
|
56
|
-
function
|
|
57
|
-
const [
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
function useBrainState() {
|
|
87
|
+
const [state, setState] = React.useState<BrainState>("idle");
|
|
88
|
+
const [intensity, setIntensity] = React.useState(0.58);
|
|
89
|
+
|
|
90
|
+
const setBrain = React.useCallback((next: BrainState, nextIntensity?: number) => {
|
|
91
|
+
setState(next);
|
|
92
|
+
if (nextIntensity !== undefined) setIntensity(clamp(nextIntensity, 0.38, 1));
|
|
93
|
+
}, []);
|
|
94
|
+
|
|
95
|
+
return { state, intensity, setBrain };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function BrainHome({
|
|
99
|
+
brainState,
|
|
100
|
+
intensity,
|
|
101
|
+
onBrainChange,
|
|
102
|
+
}: {
|
|
103
|
+
brainState: BrainState;
|
|
104
|
+
intensity: number;
|
|
105
|
+
onBrainChange: (state: BrainState, intensity?: number) => void;
|
|
106
|
+
}) {
|
|
107
|
+
const qc = useQueryClient();
|
|
108
|
+
const [messages, setMessages] = React.useState<Message[]>([]);
|
|
109
|
+
const [draft, setDraft] = React.useState("");
|
|
110
|
+
const [imageData, setImageData] = React.useState<string | null>(null);
|
|
111
|
+
const [streaming, setStreaming] = React.useState(false);
|
|
112
|
+
const [conversationId, setConversationId] = React.useState<string | null>(null);
|
|
113
|
+
const [explorationDepth, setExplorationDepth] = React.useState<BrainDepth>(1);
|
|
114
|
+
const [graphSearch, setGraphSearch] = React.useState("");
|
|
115
|
+
const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
|
|
116
|
+
const streamRef = React.useRef<HTMLDivElement>(null);
|
|
117
|
+
const recallTimerRef = React.useRef<number | null>(null);
|
|
118
|
+
|
|
119
|
+
const memoriesQ = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
|
|
120
|
+
const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
|
|
121
|
+
const graphQ = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
|
|
122
|
+
const modelsQ = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
|
|
123
|
+
|
|
124
|
+
const memoryFragments = React.useMemo(
|
|
125
|
+
() => buildMemoryFragments(memoriesQ.data?.data, historyQ.data?.data),
|
|
126
|
+
[memoriesQ.data, historyQ.data],
|
|
127
|
+
);
|
|
128
|
+
const graphModel = React.useMemo(() => parseKnowledgeGraph(graphQ.data?.data), [graphQ.data]);
|
|
129
|
+
const knowledgeConcepts = React.useMemo(
|
|
130
|
+
() => graphModel.nodes.slice(0, 10),
|
|
131
|
+
[graphModel.nodes],
|
|
132
|
+
);
|
|
133
|
+
const relationshipThreads = React.useMemo(
|
|
134
|
+
() => graphModel.edges.slice(0, 10),
|
|
135
|
+
[graphModel.edges],
|
|
136
|
+
);
|
|
137
|
+
const modelName = React.useMemo(() => currentModelName(modelsQ.data?.data), [modelsQ.data]);
|
|
138
|
+
const currentDepth = DEPTHS[explorationDepth - 1];
|
|
62
139
|
|
|
63
140
|
React.useEffect(() => {
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
141
|
+
if (streaming) onBrainChange("thinking", 0.94);
|
|
142
|
+
else if (draft.trim().length > 4) onBrainChange("listening", 0.76);
|
|
143
|
+
else onBrainChange(currentDepth.state, explorationDepth === 1 ? 0.58 : 0.66 + explorationDepth * 0.06);
|
|
144
|
+
}, [streaming, draft, currentDepth.state, explorationDepth, onBrainChange]);
|
|
145
|
+
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
const stream = streamRef.current;
|
|
148
|
+
if (stream) stream.scrollTop = stream.scrollHeight;
|
|
149
|
+
}, [messages]);
|
|
150
|
+
|
|
151
|
+
React.useEffect(() => {
|
|
152
|
+
return () => {
|
|
153
|
+
if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
|
|
67
154
|
};
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
async function send() {
|
|
158
|
+
const text = draft.trim();
|
|
159
|
+
if (!text || streaming) return;
|
|
160
|
+
const activeConversationId = conversationId || `brain-${Date.now()}`;
|
|
161
|
+
if (!conversationId) setConversationId(activeConversationId);
|
|
162
|
+
|
|
163
|
+
setMessages((items) => [...items, { role: "user", content: text }, { role: "assistant", content: "" }]);
|
|
164
|
+
setDraft("");
|
|
165
|
+
setImageData(null);
|
|
166
|
+
setStreaming(true);
|
|
167
|
+
onBrainChange("thinking", 0.96);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const result = await latticeApi.streamChat(
|
|
171
|
+
{ message: text, conversation_id: activeConversationId, image_data: imageData || undefined },
|
|
172
|
+
{
|
|
173
|
+
onChunk: (_delta, fullText) => {
|
|
174
|
+
setMessages((items) => {
|
|
175
|
+
const next = [...items];
|
|
176
|
+
next[next.length - 1] = { role: "assistant", content: fullText };
|
|
177
|
+
return next;
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
onTrace: (trace) => {
|
|
181
|
+
if (!trace) return;
|
|
182
|
+
onBrainChange("recalling", 0.9);
|
|
183
|
+
triggerBrainRecall();
|
|
184
|
+
if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
|
|
185
|
+
recallTimerRef.current = window.setTimeout(() => onBrainChange("thinking", 0.9), 900);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
if (result.error) {
|
|
190
|
+
setMessages((items) => {
|
|
191
|
+
const next = [...items];
|
|
192
|
+
next[next.length - 1] = { role: "assistant", content: `Unavailable: ${result.error}` };
|
|
193
|
+
return next;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
} finally {
|
|
197
|
+
setStreaming(false);
|
|
198
|
+
void qc.invalidateQueries({ queryKey: ["chatHistory"] });
|
|
199
|
+
void qc.invalidateQueries({ queryKey: ["memoryManager"] });
|
|
200
|
+
void qc.invalidateQueries({ queryKey: ["graph"] });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function deepen() {
|
|
205
|
+
setExplorationDepth((depth) => {
|
|
206
|
+
const next = Math.min(5, depth + 1) as BrainDepth;
|
|
207
|
+
const nextDepth = DEPTHS[next - 1];
|
|
208
|
+
onBrainChange(nextDepth.state, 0.66 + next * 0.06);
|
|
209
|
+
if (next >= 2) triggerBrainRecall();
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function surface() {
|
|
215
|
+
setExplorationDepth(1);
|
|
216
|
+
setSelectedGraphId(null);
|
|
217
|
+
setGraphSearch("");
|
|
218
|
+
onBrainChange("idle", 0.58);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function recallMemory(fragment: MemoryFragment) {
|
|
222
|
+
triggerBrainRecall();
|
|
223
|
+
setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
|
|
224
|
+
setMessages((items) => [
|
|
225
|
+
...items,
|
|
226
|
+
{ role: "assistant", content: `I am recalling ${fragment.kind.toLowerCase()}: ${fragment.title}` },
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
71
229
|
|
|
72
|
-
if (!open) return null;
|
|
73
230
|
return (
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
<div className="
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
231
|
+
<main className="brain-home" aria-label="Lattice Brain">
|
|
232
|
+
<section className="brain-presence" aria-label="Brain exploration">
|
|
233
|
+
<div className="brain-exploration" data-depth={explorationDepth}>
|
|
234
|
+
<LivingBrain
|
|
235
|
+
state={brainState}
|
|
236
|
+
intensity={intensity + explorationDepth * 0.035}
|
|
237
|
+
size="large"
|
|
238
|
+
depth={explorationDepth}
|
|
239
|
+
showLabel={false}
|
|
240
|
+
onInteract={deepen}
|
|
241
|
+
/>
|
|
242
|
+
|
|
243
|
+
<div className="brain-depth-badge" aria-live="polite">
|
|
244
|
+
<span>Level {explorationDepth}</span>
|
|
245
|
+
<strong>{currentDepth.label}</strong>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
|
|
249
|
+
<DepthEmergence
|
|
250
|
+
depth={explorationDepth}
|
|
251
|
+
memories={memoryFragments}
|
|
252
|
+
concepts={knowledgeConcepts}
|
|
253
|
+
relationships={relationshipThreads}
|
|
254
|
+
graphModel={graphModel}
|
|
255
|
+
graphSearch={graphSearch}
|
|
256
|
+
selectedGraphId={selectedGraphId}
|
|
257
|
+
onGraphSearch={setGraphSearch}
|
|
258
|
+
onSelectGraphNode={setSelectedGraphId}
|
|
259
|
+
onRecallMemory={recallMemory}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{explorationDepth > 1 ? (
|
|
264
|
+
<button className="brain-surface-control" type="button" onClick={surface}>
|
|
265
|
+
Surface
|
|
266
|
+
</button>
|
|
267
|
+
) : null}
|
|
80
268
|
</div>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
269
|
+
</section>
|
|
270
|
+
|
|
271
|
+
<section className="brain-conversation" aria-label="Conversation">
|
|
272
|
+
<div className="brain-conversation-header">
|
|
273
|
+
<div>
|
|
274
|
+
<h1>Lattice Brain</h1>
|
|
275
|
+
<span>{currentDepth.label}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div>{modelName}</div>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div ref={streamRef} className="brain-stream">
|
|
281
|
+
{messages.length === 0 ? (
|
|
282
|
+
<div className="mind-empty">
|
|
283
|
+
<div className="mind-empty-kicker">Begin</div>
|
|
284
|
+
<div>What are you thinking about?</div>
|
|
285
|
+
</div>
|
|
286
|
+
) : (
|
|
287
|
+
messages.map((message, index) => (
|
|
288
|
+
<div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
|
|
289
|
+
<div className="brain-message-bubble">{message.content}</div>
|
|
290
|
+
</div>
|
|
291
|
+
))
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className="brain-composer">
|
|
296
|
+
<textarea
|
|
297
|
+
value={draft}
|
|
298
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
299
|
+
onKeyDown={(event) => {
|
|
300
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
void send();
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
placeholder="Talk to your Brain..."
|
|
306
|
+
/>
|
|
307
|
+
<div className="brain-composer-actions">
|
|
308
|
+
<label className="brain-image-input">
|
|
309
|
+
<ImagePlus className="h-3.5 w-3.5" />
|
|
310
|
+
<span>Image</span>
|
|
311
|
+
<input
|
|
312
|
+
type="file"
|
|
313
|
+
accept="image/*"
|
|
314
|
+
className="sr-only"
|
|
315
|
+
onChange={async (event) => {
|
|
316
|
+
const file = event.target.files?.[0];
|
|
317
|
+
if (file) setImageData(await fileToDataUrl(file));
|
|
90
318
|
}}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</button>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
319
|
+
/>
|
|
320
|
+
</label>
|
|
321
|
+
{imageData ? <span className="brain-quiet-success">Image attached</span> : null}
|
|
322
|
+
<Button onClick={() => void send()} disabled={!draft.trim() || streaming} className="rounded-full px-5">
|
|
323
|
+
<Send className="h-4 w-4" /> Send
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
101
326
|
</div>
|
|
102
|
-
</
|
|
103
|
-
</
|
|
327
|
+
</section>
|
|
328
|
+
</main>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function DepthEmergence({
|
|
333
|
+
depth,
|
|
334
|
+
memories,
|
|
335
|
+
concepts,
|
|
336
|
+
relationships,
|
|
337
|
+
graphModel,
|
|
338
|
+
graphSearch,
|
|
339
|
+
selectedGraphId,
|
|
340
|
+
onGraphSearch,
|
|
341
|
+
onSelectGraphNode,
|
|
342
|
+
onRecallMemory,
|
|
343
|
+
}: {
|
|
344
|
+
depth: BrainDepth;
|
|
345
|
+
memories: MemoryFragment[];
|
|
346
|
+
concepts: KnowledgeConcept[];
|
|
347
|
+
relationships: RelationshipThread[];
|
|
348
|
+
graphModel: KnowledgeGraphModel;
|
|
349
|
+
graphSearch: string;
|
|
350
|
+
selectedGraphId: string | null;
|
|
351
|
+
onGraphSearch: (value: string) => void;
|
|
352
|
+
onSelectGraphNode: (id: string | null) => void;
|
|
353
|
+
onRecallMemory: (fragment: MemoryFragment) => void;
|
|
354
|
+
}) {
|
|
355
|
+
if (depth === 1) return null;
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<>
|
|
359
|
+
{depth >= 2 ? (
|
|
360
|
+
<MemoryLayer memories={memories} depth={depth} onRecallMemory={onRecallMemory} />
|
|
361
|
+
) : null}
|
|
362
|
+
{depth >= 3 && depth < 5 ? (
|
|
363
|
+
<KnowledgeLayer concepts={concepts} depth={depth} />
|
|
364
|
+
) : null}
|
|
365
|
+
{depth >= 4 && depth < 5 ? (
|
|
366
|
+
<RelationshipLayer concepts={concepts} relationships={relationships} />
|
|
367
|
+
) : null}
|
|
368
|
+
{depth >= 5 ? (
|
|
369
|
+
<EmergentKnowledgeGraph
|
|
370
|
+
model={graphModel}
|
|
371
|
+
search={graphSearch}
|
|
372
|
+
selectedId={selectedGraphId}
|
|
373
|
+
onSearch={onGraphSearch}
|
|
374
|
+
onSelect={onSelectGraphNode}
|
|
375
|
+
/>
|
|
376
|
+
) : null}
|
|
377
|
+
</>
|
|
104
378
|
);
|
|
105
379
|
}
|
|
106
380
|
|
|
107
|
-
function
|
|
381
|
+
function MemoryLayer({
|
|
382
|
+
memories,
|
|
383
|
+
depth,
|
|
384
|
+
onRecallMemory,
|
|
385
|
+
}: {
|
|
386
|
+
memories: MemoryFragment[];
|
|
387
|
+
depth: BrainDepth;
|
|
388
|
+
onRecallMemory: (fragment: MemoryFragment) => void;
|
|
389
|
+
}) {
|
|
390
|
+
const visible = memories.slice(0, depth >= 3 ? 8 : 6);
|
|
391
|
+
if (!visible.length) return <div className="memory-fragment is-empty">Memory is quiet</div>;
|
|
392
|
+
|
|
108
393
|
return (
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
const
|
|
112
|
-
const selected = active === item.id;
|
|
394
|
+
<>
|
|
395
|
+
{visible.map((memory, index) => {
|
|
396
|
+
const point = polarPoint(index, visible.length, depth >= 3 ? 39 : 31, depth >= 3 ? 24 : 18, -112);
|
|
113
397
|
return (
|
|
114
398
|
<button
|
|
115
|
-
key={
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}}
|
|
121
|
-
aria-current={selected ? "page" : undefined}
|
|
399
|
+
key={memory.id}
|
|
400
|
+
type="button"
|
|
401
|
+
className="memory-fragment"
|
|
402
|
+
style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 55}ms` })}
|
|
403
|
+
onClick={() => onRecallMemory(memory)}
|
|
122
404
|
>
|
|
123
|
-
<
|
|
124
|
-
<
|
|
405
|
+
<span>{memory.kind}</span>
|
|
406
|
+
<strong>{memory.title}</strong>
|
|
125
407
|
</button>
|
|
126
408
|
);
|
|
127
409
|
})}
|
|
128
|
-
|
|
410
|
+
</>
|
|
129
411
|
);
|
|
130
412
|
}
|
|
131
413
|
|
|
132
|
-
function
|
|
414
|
+
function KnowledgeLayer({ concepts, depth }: { concepts: KnowledgeConcept[]; depth: BrainDepth }) {
|
|
415
|
+
const visible = concepts.slice(0, depth >= 4 ? 10 : 7);
|
|
416
|
+
if (!visible.length) return <div className="concept-signal is-empty">Knowledge is forming</div>;
|
|
417
|
+
|
|
133
418
|
return (
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
419
|
+
<>
|
|
420
|
+
{visible.map((concept, index) => {
|
|
421
|
+
const point = polarPoint(index, visible.length, 24, 15, -70);
|
|
422
|
+
return (
|
|
423
|
+
<button
|
|
424
|
+
key={concept.id}
|
|
425
|
+
type="button"
|
|
426
|
+
className="concept-signal"
|
|
427
|
+
style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 45}ms` })}
|
|
428
|
+
title={concept.summary || concept.type}
|
|
429
|
+
>
|
|
430
|
+
<span>{concept.type}</span>
|
|
431
|
+
{concept.label}
|
|
432
|
+
</button>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
</>
|
|
146
436
|
);
|
|
147
437
|
}
|
|
148
438
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
439
|
+
function RelationshipLayer({
|
|
440
|
+
concepts,
|
|
441
|
+
relationships,
|
|
442
|
+
}: {
|
|
443
|
+
concepts: KnowledgeConcept[];
|
|
444
|
+
relationships: RelationshipThread[];
|
|
445
|
+
}) {
|
|
446
|
+
const visibleConcepts = concepts.slice(0, 10);
|
|
447
|
+
const layout = layoutGraphNodes(visibleConcepts, 30, 20);
|
|
448
|
+
const positionById = new Map(layout.map((item) => [item.node.id, item]));
|
|
449
|
+
const visibleRelationships = relationships
|
|
450
|
+
.map((relationship, index) => {
|
|
451
|
+
const source = positionById.get(relationship.source) || layout[index % Math.max(layout.length, 1)];
|
|
452
|
+
const target = positionById.get(relationship.target) || layout[(index + 3) % Math.max(layout.length, 1)];
|
|
453
|
+
return source && target && source.node.id !== target.node.id ? { relationship, source, target } : null;
|
|
454
|
+
})
|
|
455
|
+
.filter(Boolean)
|
|
456
|
+
.slice(0, 8) as Array<{
|
|
457
|
+
relationship: RelationshipThread;
|
|
458
|
+
source: ReturnType<typeof layoutGraphNodes>[number];
|
|
459
|
+
target: ReturnType<typeof layoutGraphNodes>[number];
|
|
460
|
+
}>;
|
|
162
461
|
|
|
163
|
-
|
|
164
|
-
document.documentElement.dataset.theme = theme;
|
|
165
|
-
}, [theme]);
|
|
462
|
+
if (!visibleRelationships.length) return null;
|
|
166
463
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
464
|
+
return (
|
|
465
|
+
<svg className="relationship-weave" viewBox="0 0 100 100" aria-hidden>
|
|
466
|
+
{visibleRelationships.map(({ relationship, source, target }, index) => (
|
|
467
|
+
<line
|
|
468
|
+
key={`${relationship.id}-${index}`}
|
|
469
|
+
x1={source.x}
|
|
470
|
+
y1={source.y}
|
|
471
|
+
x2={target.x}
|
|
472
|
+
y2={target.y}
|
|
473
|
+
style={{ animationDelay: `${index * 80}ms` }}
|
|
474
|
+
/>
|
|
475
|
+
))}
|
|
476
|
+
</svg>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
177
479
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
480
|
+
function EmergentKnowledgeGraph({
|
|
481
|
+
model,
|
|
482
|
+
search,
|
|
483
|
+
selectedId,
|
|
484
|
+
onSearch,
|
|
485
|
+
onSelect,
|
|
486
|
+
}: {
|
|
487
|
+
model: KnowledgeGraphModel;
|
|
488
|
+
search: string;
|
|
489
|
+
selectedId: string | null;
|
|
490
|
+
onSearch: (value: string) => void;
|
|
491
|
+
onSelect: (id: string | null) => void;
|
|
492
|
+
}) {
|
|
493
|
+
const query = search.trim().toLowerCase();
|
|
494
|
+
const visibleNodes = React.useMemo(() => {
|
|
495
|
+
const filtered = model.nodes.filter((node) => {
|
|
496
|
+
if (!query) return true;
|
|
497
|
+
return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
|
|
498
|
+
});
|
|
499
|
+
return filtered.slice(0, 18);
|
|
500
|
+
}, [model.nodes, query]);
|
|
501
|
+
const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
|
|
502
|
+
const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
|
|
503
|
+
const visibleEdges = React.useMemo(
|
|
504
|
+
() => model.edges.filter((edge) => positionById.has(edge.source) && positionById.has(edge.target)).slice(0, 36),
|
|
505
|
+
[model.edges, positionById],
|
|
506
|
+
);
|
|
507
|
+
const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
|
|
186
508
|
|
|
187
509
|
return (
|
|
188
|
-
<
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
<div className="brand-lockup">
|
|
194
|
-
<button className="mobile-menu" onClick={() => setDrawer(true)} aria-label="Open navigation"><Menu className="h-5 w-5" /></button>
|
|
195
|
-
<button className="brand-mark" onClick={() => go("brain")} aria-label="Open Lattice home">
|
|
196
|
-
<BrainCircuit className="h-5 w-5" />
|
|
197
|
-
</button>
|
|
198
|
-
<div className="brand-copy">
|
|
199
|
-
<div className="brand-name">Lattice</div>
|
|
200
|
-
<div className="brand-subtitle">Digital Brain</div>
|
|
201
|
-
</div>
|
|
510
|
+
<section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label="Knowledge Graph">
|
|
511
|
+
<div className="brain-graph-head">
|
|
512
|
+
<div>
|
|
513
|
+
<span>Level 5</span>
|
|
514
|
+
<strong>Knowledge Graph</strong>
|
|
202
515
|
</div>
|
|
516
|
+
<label className="brain-graph-search">
|
|
517
|
+
<Search className="h-3.5 w-3.5" />
|
|
518
|
+
<input
|
|
519
|
+
value={search}
|
|
520
|
+
onChange={(event) => onSearch(event.target.value)}
|
|
521
|
+
placeholder="Search"
|
|
522
|
+
aria-label="Search knowledge graph"
|
|
523
|
+
/>
|
|
524
|
+
</label>
|
|
525
|
+
</div>
|
|
203
526
|
|
|
204
|
-
|
|
205
|
-
|
|
527
|
+
{visibleNodes.length ? (
|
|
528
|
+
<div className="brain-graph-canvas">
|
|
529
|
+
<svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
|
|
530
|
+
{visibleEdges.map((edge, index) => {
|
|
531
|
+
const source = positionById.get(edge.source);
|
|
532
|
+
const target = positionById.get(edge.target);
|
|
533
|
+
if (!source || !target) return null;
|
|
534
|
+
return (
|
|
535
|
+
<line
|
|
536
|
+
key={`${edge.id}-${index}`}
|
|
537
|
+
x1={source.x}
|
|
538
|
+
y1={source.y}
|
|
539
|
+
x2={target.x}
|
|
540
|
+
y2={target.y}
|
|
541
|
+
style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
})}
|
|
545
|
+
</svg>
|
|
546
|
+
{layout.map(({ node, x, y }, index) => (
|
|
547
|
+
<button
|
|
548
|
+
key={node.id}
|
|
549
|
+
type="button"
|
|
550
|
+
className={`graph-node ${selected?.id === node.id ? "is-selected" : ""}`}
|
|
551
|
+
style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
|
|
552
|
+
onClick={() => onSelect(node.id)}
|
|
553
|
+
>
|
|
554
|
+
<span>{node.type}</span>
|
|
555
|
+
{node.label}
|
|
556
|
+
</button>
|
|
557
|
+
))}
|
|
206
558
|
</div>
|
|
559
|
+
) : (
|
|
560
|
+
<div className="brain-graph-empty">No matching knowledge yet</div>
|
|
561
|
+
)}
|
|
207
562
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
<span>{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
</
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<button className="drawer-scrim" aria-label="Close navigation" onClick={() => setDrawer(false)} />
|
|
223
|
-
<div className="drawer-panel">
|
|
224
|
-
<div className="drawer-header">
|
|
225
|
-
<div>
|
|
226
|
-
<div className="font-semibold">Lattice</div>
|
|
227
|
-
<div className="text-xs text-muted-foreground">Choose a room</div>
|
|
228
|
-
</div>
|
|
229
|
-
<Button variant="ghost" size="icon" onClick={() => setDrawer(false)} aria-label="Close navigation"><X className="h-4 w-4" /></Button>
|
|
230
|
-
</div>
|
|
231
|
-
<PrimaryDock active={route.primary} onNavigate={() => setDrawer(false)} />
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
) : null}
|
|
563
|
+
<div className="brain-graph-focus">
|
|
564
|
+
{selected ? (
|
|
565
|
+
<>
|
|
566
|
+
<span>{selected.type}</span>
|
|
567
|
+
<strong>{selected.label}</strong>
|
|
568
|
+
<p>{selected.summary || "This concept is part of the deepest knowledge layer."}</p>
|
|
569
|
+
</>
|
|
570
|
+
) : (
|
|
571
|
+
<p>Capture documents, conversations, or projects to grow the graph.</p>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
</section>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
235
577
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
</div>
|
|
252
|
-
</section>
|
|
578
|
+
function buildMemoryFragments(memoryData: unknown, historyData: unknown): MemoryFragment[] {
|
|
579
|
+
const memory = isRecord(memoryData) ? memoryData : {};
|
|
580
|
+
const sourceRows = asArray<ApiRecord>(memory.sources).length
|
|
581
|
+
? asArray<ApiRecord>(memory.sources)
|
|
582
|
+
: asArray<ApiRecord>(memory.tiers);
|
|
583
|
+
const sourceFragments = sourceRows.map((item, index) => ({
|
|
584
|
+
id: textValue(item, ["id", "source", "label"], `memory-${index}`),
|
|
585
|
+
title: textValue(item, ["title", "label", "source", "path", "name"], "Workspace memory"),
|
|
586
|
+
kind: titleValue(item, ["type", "source_type", "kind", "health"], "Memory"),
|
|
587
|
+
}));
|
|
588
|
+
const conversationFragments = asArray<ApiRecord>(historyData).map((item, index) => ({
|
|
589
|
+
id: textValue(item, ["id", "conversation_id"], `conversation-${index}`),
|
|
590
|
+
title: textValue(item, ["title", "summary", "id"], "Conversation"),
|
|
591
|
+
kind: "Conversation",
|
|
592
|
+
}));
|
|
253
593
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
594
|
+
return uniqueById([...sourceFragments, ...conversationFragments]).slice(0, 10);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function parseKnowledgeGraph(data: unknown): KnowledgeGraphModel {
|
|
598
|
+
const graph = isRecord(data) ? data : {};
|
|
599
|
+
const rawNodes = asArray<ApiRecord>(graph.nodes);
|
|
600
|
+
const rawEdges = asArray<ApiRecord>(graph.edges);
|
|
601
|
+
const nodes = rawNodes.flatMap((node): KnowledgeConcept[] => {
|
|
602
|
+
const id = textValue(node, ["id", "node_id", "title", "label"]);
|
|
603
|
+
if (!id) return [];
|
|
604
|
+
const metadata = isRecord(node.metadata) ? node.metadata : {};
|
|
605
|
+
const type = titleValue(node, ["type", "kind", "category"], "Concept");
|
|
606
|
+
const label = textValue(node, ["title", "label", "name"], id.replace(/^[^:]+:/, ""));
|
|
607
|
+
const summary = textValue(node, ["summary", "description", "snippet"]) || textValue(metadata, ["summary", "description", "relative_path", "filename"]);
|
|
608
|
+
const importance = clamp(numberValue(node, ["importance_norm", "importance", "score"]) || 0.5, 0.08, 1);
|
|
609
|
+
return [{ id, label, type, summary, importance }];
|
|
610
|
+
}).sort((left, right) => right.importance - left.importance);
|
|
611
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
612
|
+
const edges = rawEdges.flatMap((edge, index): RelationshipThread[] => {
|
|
613
|
+
const source = textValue(edge, ["from", "source", "source_id"]);
|
|
614
|
+
const target = textValue(edge, ["to", "target", "target_id"]);
|
|
615
|
+
if (!source || !target || !ids.has(source) || !ids.has(target)) return [];
|
|
616
|
+
return [{
|
|
617
|
+
id: textValue(edge, ["id"], `edge-${index}`),
|
|
618
|
+
source,
|
|
619
|
+
target,
|
|
620
|
+
label: titleValue(edge, ["type", "label", "relationship"], "Relates"),
|
|
621
|
+
weight: numberValue(edge, ["weight", "score", "confidence"]) || 1,
|
|
622
|
+
}];
|
|
623
|
+
});
|
|
624
|
+
return { nodes, edges };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function currentModelName(data: unknown) {
|
|
628
|
+
const record = isRecord(data) ? data : {};
|
|
629
|
+
const current = textValue(record, ["current", "current_model", "local_model"]);
|
|
630
|
+
if (current) return current;
|
|
631
|
+
const loaded = asArray<ApiRecord>(record.loaded || record.loaded_models);
|
|
632
|
+
const firstLoaded = loaded.find((item) => item.id || item.name || item.model_id);
|
|
633
|
+
return firstLoaded ? textValue(firstLoaded, ["name", "id", "model_id"], "local mind") : "local mind";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function fileToDataUrl(file: File) {
|
|
637
|
+
return new Promise<string>((resolve, reject) => {
|
|
638
|
+
const reader = new FileReader();
|
|
639
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
640
|
+
reader.onerror = () => reject(reader.error);
|
|
641
|
+
reader.readAsDataURL(file);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function layoutGraphNodes(nodes: KnowledgeConcept[], radiusX: number, radiusY: number) {
|
|
646
|
+
return nodes.map((node, index) => {
|
|
647
|
+
const point = polarPoint(index, nodes.length, radiusX, radiusY, -88);
|
|
648
|
+
return { node, x: point.x, y: point.y };
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function polarPoint(index: number, total: number, radiusX: number, radiusY: number, offsetDegrees = -90) {
|
|
653
|
+
const count = Math.max(total, 1);
|
|
654
|
+
const angle = ((360 / count) * index + offsetDegrees) * Math.PI / 180;
|
|
655
|
+
return {
|
|
656
|
+
x: 50 + Math.cos(angle) * radiusX,
|
|
657
|
+
y: 50 + Math.sin(angle) * radiusY,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function layerStyle(values: Record<string, string>) {
|
|
662
|
+
return values as React.CSSProperties;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function uniqueById<T extends { id: string }>(items: T[]) {
|
|
666
|
+
const seen = new Set<string>();
|
|
667
|
+
return items.filter((item) => {
|
|
668
|
+
if (seen.has(item.id)) return false;
|
|
669
|
+
seen.add(item.id);
|
|
670
|
+
return true;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function isRecord(value: unknown): value is ApiRecord {
|
|
675
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function textValue(record: ApiRecord, keys: string[], fallback = "") {
|
|
679
|
+
for (const key of keys) {
|
|
680
|
+
const value = record[key];
|
|
681
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
682
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
683
|
+
}
|
|
684
|
+
return fallback;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function titleValue(record: ApiRecord, keys: string[], fallback = "") {
|
|
688
|
+
const value = textValue(record, keys, fallback);
|
|
689
|
+
return value
|
|
690
|
+
.replace(/[_-]+/g, " ")
|
|
691
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function numberValue(record: ApiRecord, keys: string[]) {
|
|
695
|
+
for (const key of keys) {
|
|
696
|
+
const value = Number(record[key]);
|
|
697
|
+
if (Number.isFinite(value)) return value;
|
|
698
|
+
}
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function clamp(value: number, min: number, max: number) {
|
|
703
|
+
return Math.max(min, Math.min(max, value));
|
|
259
704
|
}
|