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.
- package/README.md +36 -38
- package/docs/CHANGELOG.md +75 -1
- package/frontend/src/App.tsx +3 -1286
- package/frontend/src/components/LanguageSwitcher.tsx +23 -0
- package/frontend/src/components/ProductFlow.tsx +32 -669
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +127 -0
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +29 -0
- package/frontend/src/components/onboarding/InstallScreen.tsx +208 -0
- package/frontend/src/components/onboarding/LanguageChooser.tsx +23 -0
- package/frontend/src/components/onboarding/LoginScreen.tsx +131 -0
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +13 -0
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +59 -0
- package/frontend/src/components/onboarding/recommendationModel.ts +132 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainCarePanel.tsx +254 -0
- package/frontend/src/features/brain/BrainComposer.tsx +66 -0
- package/frontend/src/features/brain/BrainConversation.tsx +131 -0
- package/frontend/src/features/brain/BrainGraphLayer.tsx +132 -0
- package/frontend/src/features/brain/BrainHome.tsx +232 -0
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +38 -0
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +74 -0
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +43 -0
- package/frontend/src/features/brain/DepthEmergence.tsx +53 -0
- package/frontend/src/features/brain/brainData.ts +98 -0
- package/frontend/src/features/brain/graphLayout.ts +26 -0
- package/frontend/src/features/brain/types.ts +44 -0
- package/frontend/src/features/review/ReviewCard.tsx +3 -1
- package/frontend/src/features/review/ReviewInbox.tsx +11 -1
- package/frontend/src/i18n.ts +290 -0
- package/frontend/src/pages/Brain.tsx +63 -5
- package/frontend/src/pages/Capture.tsx +102 -13
- package/frontend/src/pages/Library.tsx +72 -6
- package/frontend/src/styles.css +220 -0
- 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/tools.py +50 -23
- package/latticeai/app_factory.py +59 -76
- package/latticeai/cli/entrypoint.py +283 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot.py +1009 -0
- package/latticeai/runtime/chat_wiring.py +119 -0
- package/latticeai/runtime/lifespan_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +40 -0
- package/latticeai/runtime/platform_runtime_wiring.py +86 -0
- package/latticeai/runtime/review_wiring.py +37 -0
- package/latticeai/runtime/router_registration.py +49 -30
- package/latticeai/runtime/tail_wiring.py +21 -0
- package/latticeai/services/p_reinforce.py +258 -0
- package/latticeai/services/router_context.py +52 -0
- package/ltcai_cli.py +7 -279
- package/p_reinforce.py +4 -255
- package/package.json +3 -2
- package/scripts/check_i18n_literals.mjs +52 -0
- package/scripts/release_smoke.py +133 -0
- package/scripts/wheel_smoke.py +4 -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 +5 -5
- package/static/app/assets/index-D76dWuQk.js +16 -0
- package/static/app/assets/index-D76dWuQk.js.map +1 -0
- package/static/app/assets/index-Div5vMlq.css +2 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1002
- package/static/app/assets/index-B744yblP.css +0 -2
- package/static/app/assets/index-DYaUKNfl.js +0 -16
- 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
|
+
}
|