ltcai 6.1.0 → 6.3.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 (70) hide show
  1. package/README.md +36 -38
  2. package/docs/CHANGELOG.md +75 -1
  3. package/frontend/src/App.tsx +3 -1286
  4. package/frontend/src/components/LanguageSwitcher.tsx +23 -0
  5. package/frontend/src/components/ProductFlow.tsx +32 -669
  6. package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
  7. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
  8. package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
  9. package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
  10. package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
  11. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -0
  12. package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
  13. package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
  14. package/frontend/src/features/admin/AdminConsole.tsx +294 -0
  15. package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
  16. package/frontend/src/features/brain/BrainComposer.tsx +66 -0
  17. package/frontend/src/features/brain/BrainConversation.tsx +131 -0
  18. package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
  19. package/frontend/src/features/brain/BrainHome.tsx +232 -0
  20. package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
  21. package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
  22. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
  23. package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
  24. package/frontend/src/features/brain/brainData.ts +98 -0
  25. package/frontend/src/features/brain/graphLayout.ts +26 -0
  26. package/frontend/src/features/brain/types.ts +44 -0
  27. package/frontend/src/features/review/ReviewCard.tsx +3 -1
  28. package/frontend/src/features/review/ReviewInbox.tsx +11 -1
  29. package/frontend/src/i18n.ts +290 -0
  30. package/frontend/src/pages/Brain.tsx +63 -5
  31. package/frontend/src/pages/Capture.tsx +102 -13
  32. package/frontend/src/pages/Library.tsx +72 -6
  33. package/frontend/src/styles.css +220 -0
  34. package/lattice_brain/__init__.py +1 -1
  35. package/lattice_brain/runtime/multi_agent.py +1 -1
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/tools.py +50 -23
  38. package/latticeai/app_factory.py +59 -76
  39. package/latticeai/cli/entrypoint.py +283 -0
  40. package/latticeai/core/marketplace.py +1 -1
  41. package/latticeai/core/workspace_os.py +1 -1
  42. package/latticeai/integrations/__init__.py +0 -0
  43. package/latticeai/integrations/telegram_bot.py +1009 -0
  44. package/latticeai/runtime/chat_wiring.py +119 -0
  45. package/latticeai/runtime/lifespan_runtime.py +1 -1
  46. package/latticeai/runtime/model_wiring.py +40 -0
  47. package/latticeai/runtime/platform_runtime_wiring.py +86 -0
  48. package/latticeai/runtime/review_wiring.py +37 -0
  49. package/latticeai/runtime/router_registration.py +49 -30
  50. package/latticeai/runtime/tail_wiring.py +21 -0
  51. package/latticeai/services/p_reinforce.py +258 -0
  52. package/latticeai/services/router_context.py +52 -0
  53. package/ltcai_cli.py +7 -279
  54. package/p_reinforce.py +4 -255
  55. package/package.json +3 -2
  56. package/scripts/check_i18n_literals.mjs +52 -0
  57. package/scripts/release_smoke.py +133 -0
  58. package/scripts/wheel_smoke.py +4 -0
  59. package/src-tauri/Cargo.lock +1 -1
  60. package/src-tauri/Cargo.toml +1 -1
  61. package/src-tauri/tauri.conf.json +1 -1
  62. package/static/app/asset-manifest.json +5 -5
  63. package/static/app/assets/index-D76dWuQk.js +16 -0
  64. package/static/app/assets/index-D76dWuQk.js.map +1 -0
  65. package/static/app/assets/index-Div5vMlq.css +2 -0
  66. package/static/app/index.html +2 -2
  67. package/telegram_bot.py +9 -1002
  68. package/static/app/assets/index-B744yblP.css +0 -2
  69. package/static/app/assets/index-DYaUKNfl.js +0 -16
  70. package/static/app/assets/index-DYaUKNfl.js.map +0 -1
@@ -0,0 +1,66 @@
1
+ import * as React from "react";
2
+ import { ImagePlus, Send } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { t, type Language } from "@/i18n";
5
+
6
+ export function BrainComposer({
7
+ language,
8
+ draft,
9
+ streaming,
10
+ imageData,
11
+ onDraftChange,
12
+ onImageDataChange,
13
+ onSend,
14
+ }: {
15
+ language: Language;
16
+ draft: string;
17
+ streaming: boolean;
18
+ imageData: string | null;
19
+ onDraftChange: (value: string) => void;
20
+ onImageDataChange: (value: string | null) => void;
21
+ onSend: () => void;
22
+ }) {
23
+ return (
24
+ <div className="brain-composer">
25
+ <textarea
26
+ value={draft}
27
+ onChange={(event) => onDraftChange(event.target.value)}
28
+ onKeyDown={(event) => {
29
+ if (event.key === "Enter" && !event.shiftKey) {
30
+ event.preventDefault();
31
+ onSend();
32
+ }
33
+ }}
34
+ placeholder={t(language, "brain.placeholder")}
35
+ />
36
+ <div className="brain-composer-actions">
37
+ <label className="brain-image-input">
38
+ <ImagePlus className="h-3.5 w-3.5" />
39
+ <span>{t(language, "brain.image")}</span>
40
+ <input
41
+ type="file"
42
+ accept="image/*"
43
+ className="sr-only"
44
+ onChange={async (event) => {
45
+ const file = event.target.files?.[0];
46
+ if (file) onImageDataChange(await fileToDataUrl(file));
47
+ }}
48
+ />
49
+ </label>
50
+ {imageData ? <span className="brain-quiet-success">{t(language, "brain.imageAttached")}</span> : null}
51
+ <Button onClick={onSend} disabled={!draft.trim() || streaming} className="rounded-full px-5">
52
+ <Send className="h-4 w-4" /> {t(language, "brain.send")}
53
+ </Button>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function fileToDataUrl(file: File) {
60
+ return new Promise<string>((resolve, reject) => {
61
+ const reader = new FileReader();
62
+ reader.onload = () => resolve(String(reader.result || ""));
63
+ reader.onerror = () => reject(reader.error);
64
+ reader.readAsDataURL(file);
65
+ });
66
+ }
@@ -0,0 +1,131 @@
1
+ import * as React from "react";
2
+ import { ShieldCheck } from "lucide-react";
3
+ import { LanguageSwitcher } from "@/components/LanguageSwitcher";
4
+ import { t, type Language } from "@/i18n";
5
+ import type { BrainDepth, KnowledgeConcept, MemoryFragment, Message } from "./types";
6
+ import { BrainCarePanel } from "./BrainCarePanel";
7
+ import { BrainComposer } from "./BrainComposer";
8
+ import { BrainOverviewPanel } from "./BrainOverviewPanel";
9
+
10
+ export function BrainConversation({
11
+ language,
12
+ explorationDepth,
13
+ modelName,
14
+ messages,
15
+ starterPrompts,
16
+ memoryFeedback,
17
+ draft,
18
+ streaming,
19
+ imageData,
20
+ streamRef,
21
+ memories,
22
+ concepts,
23
+ onOpenDepth,
24
+ onDraftChange,
25
+ onImageDataChange,
26
+ onSend,
27
+ }: {
28
+ language: Language;
29
+ explorationDepth: BrainDepth;
30
+ modelName: string;
31
+ messages: Message[];
32
+ starterPrompts: string[];
33
+ memoryFeedback: string | null;
34
+ draft: string;
35
+ streaming: boolean;
36
+ imageData: string | null;
37
+ streamRef: React.RefObject<HTMLDivElement | null>;
38
+ memories: MemoryFragment[];
39
+ concepts: KnowledgeConcept[];
40
+ onOpenDepth: (depth: BrainDepth) => void;
41
+ onDraftChange: (value: string) => void;
42
+ onImageDataChange: (value: string | null) => void;
43
+ onSend: () => void;
44
+ }) {
45
+ return (
46
+ <section className="brain-conversation" aria-label={t(language, "brain.aria.conversation")}>
47
+ <div className="brain-conversation-header">
48
+ <div>
49
+ <h1>{t(language, "brain.title")}</h1>
50
+ <span>{t(language, `brain.depth.${explorationDepth}`)}</span>
51
+ </div>
52
+ <LanguageSwitcher compact />
53
+ <div className="brain-ownership-strip" aria-label={t(language, "brain.aria.ownership")}>
54
+ <span>{t(language, "brain.local")}</span>
55
+ <span>{t(language, "brain.portable")}</span>
56
+ <span>{t(language, "brain.private")}</span>
57
+ </div>
58
+ <div>{modelName}</div>
59
+ <button className="brain-admin-link" type="button" onClick={() => navigateHash("/admin")}>
60
+ <ShieldCheck className="h-3.5 w-3.5" />
61
+ {t(language, "brain.admin")}
62
+ </button>
63
+ </div>
64
+
65
+ <div ref={streamRef} className="brain-stream">
66
+ <BrainOverviewPanel
67
+ memories={memories}
68
+ concepts={concepts}
69
+ onOpenDepth={onOpenDepth}
70
+ />
71
+ {messages.length === 0 ? (
72
+ <BrainEmptyState language={language} starterPrompts={starterPrompts} onDraftChange={onDraftChange} />
73
+ ) : (
74
+ messages.map((message, index) => (
75
+ <div key={`${message.role}-${index}`} className={`brain-message ${message.role}`}>
76
+ <div className="brain-message-bubble">{message.content}</div>
77
+ </div>
78
+ ))
79
+ )}
80
+ </div>
81
+
82
+ {memoryFeedback ? <div className="brain-save-feedback" role="status">{memoryFeedback}</div> : null}
83
+
84
+ <BrainCarePanel language={language} />
85
+
86
+ <BrainComposer
87
+ language={language}
88
+ draft={draft}
89
+ streaming={streaming}
90
+ imageData={imageData}
91
+ onDraftChange={onDraftChange}
92
+ onImageDataChange={onImageDataChange}
93
+ onSend={onSend}
94
+ />
95
+ </section>
96
+ );
97
+ }
98
+
99
+ function BrainEmptyState({
100
+ language,
101
+ starterPrompts,
102
+ onDraftChange,
103
+ }: {
104
+ language: Language;
105
+ starterPrompts: string[];
106
+ onDraftChange: (value: string) => void;
107
+ }) {
108
+ return (
109
+ <div className="mind-empty">
110
+ <div className="mind-empty-kicker">{t(language, "brain.empty.kicker")}</div>
111
+ <div className="mind-empty-title">{t(language, "brain.empty.title")}</div>
112
+ <p>{t(language, "brain.empty.body")}</p>
113
+ <div className="mind-empty-prompts" aria-label={t(language, "brain.aria.starterPrompts")}>
114
+ {starterPrompts.map((prompt) => (
115
+ <button key={prompt} type="button" onClick={() => onDraftChange(prompt)}>
116
+ {prompt}
117
+ </button>
118
+ ))}
119
+ </div>
120
+ <div className="mind-empty-trail" aria-label={t(language, "brain.empty.trail.label")}>
121
+ <span>{t(language, "brain.empty.trail.save")}</span>
122
+ <span>{t(language, "brain.empty.trail.recall")}</span>
123
+ <span>{t(language, "brain.empty.trail.backup")}</span>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ function navigateHash(route: string) {
130
+ window.location.hash = route;
131
+ }
@@ -0,0 +1,132 @@
1
+ import * as React from "react";
2
+ import { Search } from "lucide-react";
3
+ import { t } from "@/i18n";
4
+ import { useAppStore } from "@/store/appStore";
5
+ import type { KnowledgeConcept, KnowledgeGraphModel } from "./types";
6
+ import { clamp, layerStyle, layoutGraphNodes, polarPoint } from "./graphLayout";
7
+
8
+ export function BrainKnowledgeLayer({ concepts, depth }: { concepts: KnowledgeConcept[]; depth: number }) {
9
+ const language = useAppStore((state) => state.language);
10
+ const visible = concepts.slice(0, depth >= 4 ? 10 : 7);
11
+ if (!visible.length) return <div className="concept-signal is-empty">{t(language, "brain.knowledge.empty")}</div>;
12
+
13
+ return (
14
+ <>
15
+ {visible.map((concept, index) => {
16
+ const point = polarPoint(index, visible.length, 24, 15, -70);
17
+ return (
18
+ <button
19
+ key={concept.id}
20
+ type="button"
21
+ className="concept-signal"
22
+ style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 45}ms` })}
23
+ title={concept.summary || concept.type}
24
+ >
25
+ <span>{concept.type}</span>
26
+ {concept.label}
27
+ </button>
28
+ );
29
+ })}
30
+ </>
31
+ );
32
+ }
33
+
34
+ export function BrainGraphLayer({
35
+ model,
36
+ search,
37
+ selectedId,
38
+ onSearch,
39
+ onSelect,
40
+ }: {
41
+ model: KnowledgeGraphModel;
42
+ search: string;
43
+ selectedId: string | null;
44
+ onSearch: (value: string) => void;
45
+ onSelect: (id: string | null) => void;
46
+ }) {
47
+ const language = useAppStore((state) => state.language);
48
+ const query = search.trim().toLowerCase();
49
+ const visibleNodes = React.useMemo(() => {
50
+ const filtered = model.nodes.filter((node) => {
51
+ if (!query) return true;
52
+ return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
53
+ });
54
+ return filtered.slice(0, 18);
55
+ }, [model.nodes, query]);
56
+ const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
57
+ const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
58
+ const visibleEdges = React.useMemo(
59
+ () => model.edges.filter((edge) => positionById.has(edge.source) && positionById.has(edge.target)).slice(0, 36),
60
+ [model.edges, positionById],
61
+ );
62
+ const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
63
+
64
+ return (
65
+ <section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label={t(language, "brain.aria.graph")}>
66
+ <div className="brain-graph-head">
67
+ <div>
68
+ <span>{t(language, "brain.level")} 5</span>
69
+ <strong>{t(language, "brain.depth.5")}</strong>
70
+ </div>
71
+ <label className="brain-graph-search">
72
+ <Search className="h-3.5 w-3.5" />
73
+ <input
74
+ value={search}
75
+ onChange={(event) => onSearch(event.target.value)}
76
+ placeholder={t(language, "brain.graph.search")}
77
+ aria-label={t(language, "brain.graph.searchAria")}
78
+ />
79
+ </label>
80
+ </div>
81
+
82
+ {visibleNodes.length ? (
83
+ <div className="brain-graph-canvas">
84
+ <svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
85
+ {visibleEdges.map((edge, index) => {
86
+ const source = positionById.get(edge.source);
87
+ const target = positionById.get(edge.target);
88
+ if (!source || !target) return null;
89
+ return (
90
+ <line
91
+ key={`${edge.id}-${index}`}
92
+ x1={source.x}
93
+ y1={source.y}
94
+ x2={target.x}
95
+ y2={target.y}
96
+ style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
97
+ />
98
+ );
99
+ })}
100
+ </svg>
101
+ {layout.map(({ node, x, y }, index) => (
102
+ <button
103
+ key={node.id}
104
+ type="button"
105
+ className={`graph-node ${selected?.id === node.id ? "is-selected" : ""}`}
106
+ style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
107
+ onClick={() => onSelect(node.id)}
108
+ >
109
+ <span>{node.type}</span>
110
+ {node.label}
111
+ </button>
112
+ ))}
113
+ </div>
114
+ ) : (
115
+ <div className="brain-graph-empty">{t(language, "brain.graph.empty")}</div>
116
+ )}
117
+
118
+ <div className="brain-graph-focus">
119
+ {selected ? (
120
+ <>
121
+ <span>{selected.type}</span>
122
+ <strong>{selected.label}</strong>
123
+ <p>{selected.summary || t(language, "brain.graph.summaryFallback")}</p>
124
+ <p>{t(language, "brain.graph.focused")}</p>
125
+ </>
126
+ ) : (
127
+ <p>{t(language, "brain.graph.emptyFocus")}</p>
128
+ )}
129
+ </div>
130
+ </section>
131
+ );
132
+ }
@@ -0,0 +1,232 @@
1
+ import * as React from "react";
2
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
3
+ import { latticeApi } from "@/api/client";
4
+ import { type BrainState, LivingBrain, triggerBrainRecall } from "@/components/LivingBrain";
5
+ import { useAppStore } from "@/store/appStore";
6
+ import { t } from "@/i18n";
7
+ import { BrainConversation } from "./BrainConversation";
8
+ import { buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
9
+ import { DepthEmergence } from "./DepthEmergence";
10
+ import { DEPTHS, type BrainDepth, type MemoryFragment, type Message } from "./types";
11
+
12
+ export function BrainHome({
13
+ brainState,
14
+ intensity,
15
+ onBrainChange,
16
+ }: {
17
+ brainState: BrainState;
18
+ intensity: number;
19
+ onBrainChange: (state: BrainState, intensity?: number) => void;
20
+ }) {
21
+ const qc = useQueryClient();
22
+ const language = useAppStore((state) => state.language);
23
+ const [messages, setMessages] = React.useState<Message[]>([]);
24
+ const [draft, setDraft] = React.useState("");
25
+ const [imageData, setImageData] = React.useState<string | null>(null);
26
+ const [streaming, setStreaming] = React.useState(false);
27
+ const [conversationId, setConversationId] = React.useState<string | null>(null);
28
+ const [explorationDepth, setExplorationDepth] = React.useState<BrainDepth>(1);
29
+ const [graphSearch, setGraphSearch] = React.useState("");
30
+ const [selectedGraphId, setSelectedGraphId] = React.useState<string | null>(null);
31
+ const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
32
+ const streamRef = React.useRef<HTMLDivElement>(null);
33
+ const recallTimerRef = React.useRef<number | null>(null);
34
+
35
+ const memoriesQ = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
36
+ const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
37
+ const graphQ = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
38
+ const modelsQ = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
39
+
40
+ const memoryFragments = React.useMemo(
41
+ () => buildMemoryFragments(memoriesQ.data?.data, historyQ.data?.data),
42
+ [memoriesQ.data, historyQ.data],
43
+ );
44
+ const graphModel = React.useMemo(() => parseKnowledgeGraph(graphQ.data?.data), [graphQ.data]);
45
+ const knowledgeConcepts = React.useMemo(
46
+ () => graphModel.nodes.slice(0, 10),
47
+ [graphModel.nodes],
48
+ );
49
+ const relationshipThreads = React.useMemo(
50
+ () => graphModel.edges.slice(0, 10),
51
+ [graphModel.edges],
52
+ );
53
+ const modelName = React.useMemo(() => currentModelName(modelsQ.data?.data), [modelsQ.data]);
54
+ const currentDepth = DEPTHS[explorationDepth - 1];
55
+ const starterPrompts = React.useMemo(
56
+ () => [
57
+ t(language, "brain.prompt.remember"),
58
+ t(language, "brain.prompt.know"),
59
+ t(language, "brain.prompt.plan"),
60
+ ],
61
+ [language],
62
+ );
63
+
64
+ React.useEffect(() => {
65
+ if (streaming) onBrainChange("thinking", 0.94);
66
+ else if (draft.trim().length > 4) onBrainChange("listening", 0.76);
67
+ else onBrainChange(currentDepth.state, explorationDepth === 1 ? 0.58 : 0.66 + explorationDepth * 0.06);
68
+ }, [streaming, draft, currentDepth.state, explorationDepth, onBrainChange]);
69
+
70
+ React.useEffect(() => {
71
+ const stream = streamRef.current;
72
+ if (stream) stream.scrollTop = stream.scrollHeight;
73
+ }, [messages]);
74
+
75
+ React.useEffect(() => {
76
+ return () => {
77
+ if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
78
+ };
79
+ }, []);
80
+
81
+ async function send() {
82
+ const text = draft.trim();
83
+ if (!text || streaming) return;
84
+ const activeConversationId = conversationId || `brain-${Date.now()}`;
85
+ if (!conversationId) setConversationId(activeConversationId);
86
+
87
+ setMessages((items) => [...items, { role: "user", content: text }, { role: "assistant", content: "" }]);
88
+ setDraft("");
89
+ setImageData(null);
90
+ setStreaming(true);
91
+ setMemoryFeedback(null);
92
+ onBrainChange("thinking", 0.96);
93
+
94
+ try {
95
+ const result = await latticeApi.streamChat(
96
+ { message: text, conversation_id: activeConversationId, image_data: imageData || undefined },
97
+ {
98
+ onChunk: (_delta, fullText) => {
99
+ setMessages((items) => {
100
+ const next = [...items];
101
+ next[next.length - 1] = { role: "assistant", content: fullText };
102
+ return next;
103
+ });
104
+ },
105
+ onTrace: (trace) => {
106
+ if (!trace) return;
107
+ onBrainChange("recalling", 0.9);
108
+ triggerBrainRecall();
109
+ if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
110
+ recallTimerRef.current = window.setTimeout(() => onBrainChange("thinking", 0.9), 900);
111
+ },
112
+ },
113
+ );
114
+ if (result.error) {
115
+ setMessages((items) => {
116
+ const next = [...items];
117
+ next[next.length - 1] = { role: "assistant", content: `${t(language, "brain.unavailable")}: ${result.error}` };
118
+ return next;
119
+ });
120
+ } else {
121
+ setMemoryFeedback(t(language, "brain.saved", { topics: knowledgeConcepts.length, memories: memoryFragments.length }));
122
+ }
123
+ } finally {
124
+ setStreaming(false);
125
+ void qc.invalidateQueries({ queryKey: ["chatHistory"] });
126
+ void qc.invalidateQueries({ queryKey: ["memoryManager"] });
127
+ void qc.invalidateQueries({ queryKey: ["graph"] });
128
+ }
129
+ }
130
+
131
+ function deepen() {
132
+ setExplorationDepth((depth) => {
133
+ const next = Math.min(5, depth + 1) as BrainDepth;
134
+ const nextDepth = DEPTHS[next - 1];
135
+ onBrainChange(nextDepth.state, 0.66 + next * 0.06);
136
+ if (next >= 2) triggerBrainRecall();
137
+ return next;
138
+ });
139
+ }
140
+
141
+ function jumpToDepth(next: BrainDepth) {
142
+ setExplorationDepth(next);
143
+ const nextDepth = DEPTHS[next - 1];
144
+ onBrainChange(nextDepth.state, next === 1 ? 0.58 : 0.66 + next * 0.06);
145
+ if (next >= 2) triggerBrainRecall();
146
+ }
147
+
148
+ function surface() {
149
+ setExplorationDepth(1);
150
+ setSelectedGraphId(null);
151
+ setGraphSearch("");
152
+ onBrainChange("idle", 0.58);
153
+ }
154
+
155
+ function recallMemory(fragment: MemoryFragment) {
156
+ triggerBrainRecall();
157
+ setExplorationDepth((depth) => Math.max(depth, 2) as BrainDepth);
158
+ setMessages((items) => [
159
+ ...items,
160
+ { role: "assistant", content: t(language, "brain.recalled", { title: fragment.title }) },
161
+ ]);
162
+ }
163
+
164
+ return (
165
+ <main className="brain-home" aria-label={t(language, "brain.aria.home")}>
166
+ <section className="brain-presence" aria-label={t(language, "brain.aria.exploration")}>
167
+ <div className="brain-exploration" data-depth={explorationDepth}>
168
+ <LivingBrain
169
+ state={brainState}
170
+ intensity={intensity + explorationDepth * 0.035}
171
+ size="large"
172
+ depth={explorationDepth}
173
+ showLabel={false}
174
+ onInteract={deepen}
175
+ />
176
+
177
+ <div className="brain-depth-badge" aria-live="polite">
178
+ <span>{t(language, "brain.level")} {explorationDepth}</span>
179
+ <strong>{t(language, `brain.depth.${explorationDepth}`)}</strong>
180
+ </div>
181
+
182
+ <div className="brain-depth-actions" aria-label={t(language, "brain.aria.quickViews")}>
183
+ <button type="button" className={explorationDepth === 2 ? "is-active" : ""} onClick={() => jumpToDepth(2)}>{t(language, "brain.view.memories")}</button>
184
+ <button type="button" className={explorationDepth === 3 ? "is-active" : ""} onClick={() => jumpToDepth(3)}>{t(language, "brain.view.topics")}</button>
185
+ <button type="button" className={explorationDepth === 4 ? "is-active" : ""} onClick={() => jumpToDepth(4)}>{t(language, "brain.view.relationships")}</button>
186
+ <button type="button" className={explorationDepth === 5 ? "is-active" : ""} onClick={() => jumpToDepth(5)}>{t(language, "brain.view.graph")}</button>
187
+ </div>
188
+
189
+ <div className="brain-field-layer" aria-hidden={explorationDepth < 2}>
190
+ <DepthEmergence
191
+ depth={explorationDepth}
192
+ memories={memoryFragments}
193
+ concepts={knowledgeConcepts}
194
+ relationships={relationshipThreads}
195
+ graphModel={graphModel}
196
+ graphSearch={graphSearch}
197
+ selectedGraphId={selectedGraphId}
198
+ onGraphSearch={setGraphSearch}
199
+ onSelectGraphNode={setSelectedGraphId}
200
+ onRecallMemory={recallMemory}
201
+ />
202
+ </div>
203
+
204
+ {explorationDepth > 1 ? (
205
+ <button className="brain-surface-control" type="button" onClick={surface}>
206
+ {t(language, "brain.surface")}
207
+ </button>
208
+ ) : null}
209
+ </div>
210
+ </section>
211
+
212
+ <BrainConversation
213
+ language={language}
214
+ explorationDepth={explorationDepth}
215
+ modelName={modelName}
216
+ messages={messages}
217
+ starterPrompts={starterPrompts}
218
+ memoryFeedback={memoryFeedback}
219
+ draft={draft}
220
+ streaming={streaming}
221
+ imageData={imageData}
222
+ streamRef={streamRef}
223
+ memories={memoryFragments}
224
+ concepts={knowledgeConcepts}
225
+ onOpenDepth={jumpToDepth}
226
+ onDraftChange={setDraft}
227
+ onImageDataChange={setImageData}
228
+ onSend={() => void send()}
229
+ />
230
+ </main>
231
+ );
232
+ }
@@ -0,0 +1,38 @@
1
+ import type { BrainDepth, MemoryFragment } from "./types";
2
+ import { t } from "@/i18n";
3
+ import { useAppStore } from "@/store/appStore";
4
+ import { layerStyle, polarPoint } from "./graphLayout";
5
+
6
+ export function BrainMemoryLayer({
7
+ memories,
8
+ depth,
9
+ onRecallMemory,
10
+ }: {
11
+ memories: MemoryFragment[];
12
+ depth: BrainDepth;
13
+ onRecallMemory: (fragment: MemoryFragment) => void;
14
+ }) {
15
+ const language = useAppStore((state) => state.language);
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>;
18
+
19
+ return (
20
+ <>
21
+ {visible.map((memory, index) => {
22
+ const point = polarPoint(index, visible.length, depth >= 3 ? 39 : 31, depth >= 3 ? 24 : 18, -112);
23
+ return (
24
+ <button
25
+ key={memory.id}
26
+ type="button"
27
+ className="memory-fragment"
28
+ style={layerStyle({ "--x": `${point.x}%`, "--y": `${point.y}%`, "--delay": `${index * 55}ms` })}
29
+ onClick={() => onRecallMemory(memory)}
30
+ >
31
+ <span>{memory.kind}</span>
32
+ <strong>{memory.title}</strong>
33
+ </button>
34
+ );
35
+ })}
36
+ </>
37
+ );
38
+ }
@@ -0,0 +1,74 @@
1
+ import * as React from "react";
2
+ import { t } from "@/i18n";
3
+ import { useAppStore } from "@/store/appStore";
4
+ import type { BrainDepth, KnowledgeConcept, MemoryFragment } from "./types";
5
+
6
+ export function BrainOverviewPanel({
7
+ memories,
8
+ concepts,
9
+ onOpenDepth,
10
+ }: {
11
+ memories: MemoryFragment[];
12
+ concepts: KnowledgeConcept[];
13
+ onOpenDepth: (depth: BrainDepth) => void;
14
+ }) {
15
+ const language = useAppStore((state) => state.language);
16
+ const recent = memories.slice(0, 3);
17
+ const older = memories.slice(3, 6);
18
+ const topics = concepts.slice(0, 4);
19
+
20
+ return (
21
+ <section className="brain-overview-panel" aria-label={t(language, "brain.aria.overview")}>
22
+ <div className="brain-overview-head">
23
+ <div>
24
+ <span>{t(language, "brain.overview.kicker")}</span>
25
+ <strong>{t(language, "brain.overview.title")}</strong>
26
+ </div>
27
+ <button type="button" onClick={() => onOpenDepth(5)}>{t(language, "brain.overview.graph")}</button>
28
+ </div>
29
+ <div className="brain-overview-grid">
30
+ <BrainOverviewColumn
31
+ title={t(language, "brain.overview.recent")}
32
+ empty={t(language, "brain.overview.recentEmpty")}
33
+ items={recent.map((memory) => memory.title)}
34
+ onOpen={() => onOpenDepth(2)}
35
+ />
36
+ <BrainOverviewColumn
37
+ title={t(language, "brain.overview.older")}
38
+ empty={t(language, "brain.overview.olderEmpty")}
39
+ items={older.map((memory) => memory.title)}
40
+ onOpen={() => onOpenDepth(2)}
41
+ />
42
+ <BrainOverviewColumn
43
+ title={t(language, "brain.overview.topics")}
44
+ empty={t(language, "brain.overview.topicsEmpty")}
45
+ items={topics.map((concept) => concept.label)}
46
+ onOpen={() => onOpenDepth(3)}
47
+ />
48
+ </div>
49
+ </section>
50
+ );
51
+ }
52
+
53
+ function BrainOverviewColumn({
54
+ title,
55
+ empty,
56
+ items,
57
+ onOpen,
58
+ }: {
59
+ title: string;
60
+ empty: string;
61
+ items: string[];
62
+ onOpen: () => void;
63
+ }) {
64
+ return (
65
+ <button type="button" className="brain-overview-column" onClick={onOpen}>
66
+ <span>{title}</span>
67
+ {items.length ? (
68
+ items.slice(0, 3).map((item) => <strong key={item}>{item}</strong>)
69
+ ) : (
70
+ <em>{empty}</em>
71
+ )}
72
+ </button>
73
+ );
74
+ }