ltcai 7.9.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -49
- package/docs/CHANGELOG.md +35 -0
- package/docs/V3_FRONTEND.md +11 -5
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +9 -1
- package/lattice_brain/graph/projection.py +19 -5
- package/lattice_brain/graph/provenance.py +26 -4
- package/lattice_brain/graph/write_master.py +30 -3
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/app_factory.py +13 -134
- package/latticeai/core/local_embeddings.py +8 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +140 -1
- package/latticeai/core/tool_registry.py +7 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +50 -6
- package/latticeai/services/model_runtime.py +36 -2
- package/latticeai/services/product_readiness.py +19 -17
- package/package.json +2 -2
- package/scripts/build_frontend_assets.mjs +11 -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 +11 -102
- package/static/app/assets/Act-BOO66G-c.js +1 -0
- package/static/app/assets/Brain-C6lEYiD7.js +321 -0
- package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-TATXBRDw.js} +1 -2
- package/static/app/assets/Library-DK4FIp8a.js +1 -0
- package/static/app/assets/System-Bgs6Ql7x.js +1 -0
- package/static/app/assets/core-CwxXejkd.js +1 -2
- package/static/app/assets/index-Bvv79nre.js +16 -0
- package/static/app/assets/index-Dslqglia.css +2 -0
- package/static/app/assets/primitives-B70WOra0.js +1 -0
- package/static/app/assets/{textarea-BZk6ybp5.js → textarea-Czrd9gwM.js} +1 -2
- package/static/app/index.html +3 -10
- package/static/app/theme-boot.js +8 -0
- package/static/sw.js +1 -1
- package/frontend/index.html +0 -24
- package/frontend/openapi.json +0 -15425
- package/frontend/src/App.tsx +0 -243
- package/frontend/src/api/client.ts +0 -580
- package/frontend/src/api/openapi.ts +0 -17892
- package/frontend/src/components/AdminAccessGate.tsx +0 -70
- package/frontend/src/components/FeedbackState.tsx +0 -45
- package/frontend/src/components/LanguageSwitcher.tsx +0 -23
- package/frontend/src/components/LivingBrain.tsx +0 -220
- package/frontend/src/components/ProductFlow.tsx +0 -171
- package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
- package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
- package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
- package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
- package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
- package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
- package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
- package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
- package/frontend/src/components/primitives.tsx +0 -392
- package/frontend/src/components/ui/badge.tsx +0 -27
- package/frontend/src/components/ui/button.tsx +0 -37
- package/frontend/src/components/ui/card.tsx +0 -22
- package/frontend/src/components/ui/input.tsx +0 -16
- package/frontend/src/components/ui/textarea.tsx +0 -16
- package/frontend/src/features/admin/AdminConsole.tsx +0 -334
- package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
- package/frontend/src/features/brain/BrainComposer.tsx +0 -85
- package/frontend/src/features/brain/BrainConversation.tsx +0 -688
- package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
- package/frontend/src/features/brain/BrainHome.tsx +0 -624
- package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
- package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
- package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
- package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
- package/frontend/src/features/brain/brainData.ts +0 -246
- package/frontend/src/features/brain/graphLayout.ts +0 -37
- package/frontend/src/features/brain/types.ts +0 -150
- package/frontend/src/features/review/ReviewCard.tsx +0 -100
- package/frontend/src/features/review/ReviewInbox.tsx +0 -127
- package/frontend/src/features/review/reviewHelpers.ts +0 -69
- package/frontend/src/i18n.ts +0 -1303
- package/frontend/src/lib/utils.ts +0 -33
- package/frontend/src/main.tsx +0 -23
- package/frontend/src/pages/Act.tsx +0 -458
- package/frontend/src/pages/Ask.tsx +0 -14
- package/frontend/src/pages/Brain.tsx +0 -914
- package/frontend/src/pages/Capture.tsx +0 -258
- package/frontend/src/pages/Library.tsx +0 -486
- package/frontend/src/pages/System.tsx +0 -621
- package/frontend/src/routes.ts +0 -92
- package/frontend/src/store/appStore.ts +0 -94
- package/frontend/src/styles.css +0 -6579
- package/static/app/assets/Act-DOvf59ru.js +0 -2
- package/static/app/assets/Act-DOvf59ru.js.map +0 -1
- package/static/app/assets/Brain-C7_0mEiI.js +0 -322
- package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
- package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
- package/static/app/assets/Library-BJPEEm5O.js +0 -2
- package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
- package/static/app/assets/System-D6t9jo9V.js +0 -2
- package/static/app/assets/System-D6t9jo9V.js.map +0 -1
- package/static/app/assets/core-CwxXejkd.js.map +0 -1
- package/static/app/assets/index-C7g26IF6.css +0 -2
- package/static/app/assets/index-DbcEYJQ2.js +0 -17
- package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
- package/static/app/assets/primitives-CD38lt4n.js +0 -2
- package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
- package/static/app/assets/textarea-BZk6ybp5.js.map +0 -1
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { Filter, RotateCcw, Search, X } from "lucide-react";
|
|
3
|
-
import { t } from "@/i18n";
|
|
4
|
-
import { useAppStore } from "@/store/appStore";
|
|
5
|
-
import type { KnowledgeConcept, KnowledgeGraphModel } from "./types";
|
|
6
|
-
import { clamp, computeGraphNeighbors, 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
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
35
|
-
// Time-exploration presets (days back). null == all time.
|
|
36
|
-
const TIME_WINDOWS: Array<{ days: number | null }> = [{ days: 7 }, { days: 30 }, { days: 90 }, { days: null }];
|
|
37
|
-
// A node added within this window of "now" is treated as recent for highlighting.
|
|
38
|
-
const RECENT_WINDOW_MS = 7 * DAY_MS;
|
|
39
|
-
|
|
40
|
-
// Highlight occurrences of `query` inside `label` by wrapping matches in <mark>.
|
|
41
|
-
function highlightMatch(label: string, query: string): React.ReactNode {
|
|
42
|
-
if (!query) return label;
|
|
43
|
-
const lower = label.toLowerCase();
|
|
44
|
-
const target = query.toLowerCase();
|
|
45
|
-
const parts: React.ReactNode[] = [];
|
|
46
|
-
let cursor = 0;
|
|
47
|
-
let matchIndex = lower.indexOf(target, cursor);
|
|
48
|
-
let key = 0;
|
|
49
|
-
while (matchIndex !== -1 && target.length > 0) {
|
|
50
|
-
if (matchIndex > cursor) parts.push(label.slice(cursor, matchIndex));
|
|
51
|
-
parts.push(<mark key={`m-${key++}`}>{label.slice(matchIndex, matchIndex + target.length)}</mark>);
|
|
52
|
-
cursor = matchIndex + target.length;
|
|
53
|
-
matchIndex = lower.indexOf(target, cursor);
|
|
54
|
-
}
|
|
55
|
-
if (cursor < label.length) parts.push(label.slice(cursor));
|
|
56
|
-
return parts.length ? parts : label;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function BrainGraphLayer({
|
|
60
|
-
model,
|
|
61
|
-
search,
|
|
62
|
-
selectedId,
|
|
63
|
-
onSearch,
|
|
64
|
-
onSelect,
|
|
65
|
-
}: {
|
|
66
|
-
model: KnowledgeGraphModel;
|
|
67
|
-
search: string;
|
|
68
|
-
selectedId: string | null;
|
|
69
|
-
onSearch: (value: string) => void;
|
|
70
|
-
onSelect: (id: string | null) => void;
|
|
71
|
-
}) {
|
|
72
|
-
const language = useAppStore((state) => state.language);
|
|
73
|
-
const query = search.trim().toLowerCase();
|
|
74
|
-
|
|
75
|
-
// Control state local to the graph view so existing lifted search/selection
|
|
76
|
-
// behavior (and its reset in surface()) is preserved untouched.
|
|
77
|
-
const [activeTypes, setActiveTypes] = React.useState<Set<string>>(() => new Set());
|
|
78
|
-
const [timeDays, setTimeDays] = React.useState<number | null>(null);
|
|
79
|
-
const [typeaheadOpen, setTypeaheadOpen] = React.useState(false);
|
|
80
|
-
|
|
81
|
-
// Distinct entity types available in the graph (for the type filter chips).
|
|
82
|
-
const availableTypes = React.useMemo(() => {
|
|
83
|
-
const seen = new Set<string>();
|
|
84
|
-
for (const node of model.nodes) seen.add(node.type);
|
|
85
|
-
return Array.from(seen).sort((a, b) => a.localeCompare(b));
|
|
86
|
-
}, [model.nodes]);
|
|
87
|
-
|
|
88
|
-
// Drop type selections that no longer exist after a graph refresh.
|
|
89
|
-
React.useEffect(() => {
|
|
90
|
-
setActiveTypes((prev) => {
|
|
91
|
-
if (prev.size === 0) return prev;
|
|
92
|
-
const next = new Set<string>();
|
|
93
|
-
for (const type of prev) if (availableTypes.includes(type)) next.add(type);
|
|
94
|
-
return next.size === prev.size ? prev : next;
|
|
95
|
-
});
|
|
96
|
-
}, [availableTypes]);
|
|
97
|
-
|
|
98
|
-
const hasTimestamps = React.useMemo(() => model.nodes.some((node) => typeof node.createdAt === "number"), [model.nodes]);
|
|
99
|
-
const timeCutoff = timeDays === null ? null : Date.now() - timeDays * DAY_MS;
|
|
100
|
-
|
|
101
|
-
const filtersActive = activeTypes.size > 0 || timeDays !== null;
|
|
102
|
-
|
|
103
|
-
const visibleNodes = React.useMemo(() => {
|
|
104
|
-
const filtered = model.nodes.filter((node) => {
|
|
105
|
-
if (activeTypes.size > 0 && !activeTypes.has(node.type)) return false;
|
|
106
|
-
if (timeCutoff !== null && typeof node.createdAt === "number" && node.createdAt < timeCutoff) return false;
|
|
107
|
-
if (!query) return true;
|
|
108
|
-
return `${node.label} ${node.type} ${node.summary}`.toLowerCase().includes(query);
|
|
109
|
-
});
|
|
110
|
-
return filtered.slice(0, 18);
|
|
111
|
-
}, [model.nodes, query, activeTypes, timeCutoff]);
|
|
112
|
-
|
|
113
|
-
// Typeahead suggestions: label-prefix/substring matches ranked by importance.
|
|
114
|
-
const suggestions = React.useMemo(() => {
|
|
115
|
-
if (!query) return [];
|
|
116
|
-
return model.nodes
|
|
117
|
-
.filter((node) => node.label.toLowerCase().includes(query) || node.type.toLowerCase().includes(query))
|
|
118
|
-
.slice(0, 6);
|
|
119
|
-
}, [model.nodes, query]);
|
|
120
|
-
|
|
121
|
-
const layout = React.useMemo(() => layoutGraphNodes(visibleNodes, 38, 24), [visibleNodes]);
|
|
122
|
-
const positionById = React.useMemo(() => new Map(layout.map((item) => [item.node.id, item])), [layout]);
|
|
123
|
-
const visibleEdges = React.useMemo(
|
|
124
|
-
() => model.edges.filter((edge) => positionById.has(edge.source) && positionById.has(edge.target)).slice(0, 36),
|
|
125
|
-
[model.edges, positionById],
|
|
126
|
-
);
|
|
127
|
-
const selected = visibleNodes.find((node) => node.id === selectedId) || visibleNodes[0] || null;
|
|
128
|
-
|
|
129
|
-
// 1-hop neighbor set for the focused node, drives focus/dim + edge highlight.
|
|
130
|
-
const neighborIds = React.useMemo(
|
|
131
|
-
() => (selectedId ? computeGraphNeighbors(selectedId, model.edges) : new Set<string>()),
|
|
132
|
-
[selectedId, model.edges],
|
|
133
|
-
);
|
|
134
|
-
const focusActive = Boolean(selectedId) && neighborIds.size > 0;
|
|
135
|
-
|
|
136
|
-
const matchedIds = React.useMemo(() => {
|
|
137
|
-
if (!query) return new Set<string>();
|
|
138
|
-
return new Set(visibleNodes.filter((node) => node.label.toLowerCase().includes(query)).map((node) => node.id));
|
|
139
|
-
}, [visibleNodes, query]);
|
|
140
|
-
|
|
141
|
-
function focusState(nodeId: string): "true" | "false" | undefined {
|
|
142
|
-
if (!focusActive) return undefined;
|
|
143
|
-
if (nodeId === selectedId || neighborIds.has(nodeId)) return "true";
|
|
144
|
-
return "false";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<section className="mind-core-graph" data-testid="emergent-knowledge-graph" aria-label={t(language, "brain.aria.graph")}>
|
|
149
|
-
<div className="brain-graph-head">
|
|
150
|
-
<div>
|
|
151
|
-
<span>{t(language, "brain.level")} 5</span>
|
|
152
|
-
<strong>{t(language, "brain.depth.5")}</strong>
|
|
153
|
-
</div>
|
|
154
|
-
<div className="brain-graph-search-wrap">
|
|
155
|
-
<label className="brain-graph-search">
|
|
156
|
-
<Search className="h-3.5 w-3.5" />
|
|
157
|
-
<input
|
|
158
|
-
value={search}
|
|
159
|
-
onChange={(event) => {
|
|
160
|
-
onSearch(event.target.value);
|
|
161
|
-
setTypeaheadOpen(true);
|
|
162
|
-
}}
|
|
163
|
-
onFocus={() => setTypeaheadOpen(true)}
|
|
164
|
-
onBlur={() => window.setTimeout(() => setTypeaheadOpen(false), 120)}
|
|
165
|
-
placeholder={t(language, "brain.graph.search")}
|
|
166
|
-
aria-label={t(language, "brain.graph.searchAria")}
|
|
167
|
-
role="combobox"
|
|
168
|
-
aria-expanded={typeaheadOpen && Boolean(query)}
|
|
169
|
-
aria-autocomplete="list"
|
|
170
|
-
aria-controls="brain-graph-typeahead"
|
|
171
|
-
/>
|
|
172
|
-
{search ? (
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
className="brain-graph-search-clear"
|
|
176
|
-
onClick={() => onSearch("")}
|
|
177
|
-
aria-label={t(language, "brain.graph.focus.clear")}
|
|
178
|
-
>
|
|
179
|
-
<X className="h-3 w-3" />
|
|
180
|
-
</button>
|
|
181
|
-
) : null}
|
|
182
|
-
</label>
|
|
183
|
-
{typeaheadOpen && query ? (
|
|
184
|
-
<ul
|
|
185
|
-
id="brain-graph-typeahead"
|
|
186
|
-
className="brain-graph-typeahead"
|
|
187
|
-
role="listbox"
|
|
188
|
-
aria-label={t(language, "brain.graph.search.typeaheadAria")}
|
|
189
|
-
>
|
|
190
|
-
{suggestions.length ? (
|
|
191
|
-
suggestions.map((node) => (
|
|
192
|
-
<li key={node.id} role="option" aria-selected={node.id === selectedId}>
|
|
193
|
-
<button
|
|
194
|
-
type="button"
|
|
195
|
-
onMouseDown={(event) => {
|
|
196
|
-
event.preventDefault();
|
|
197
|
-
onSearch(node.label);
|
|
198
|
-
onSelect(node.id);
|
|
199
|
-
setTypeaheadOpen(false);
|
|
200
|
-
}}
|
|
201
|
-
>
|
|
202
|
-
<span>{node.type}</span>
|
|
203
|
-
<strong>{highlightMatch(node.label, query)}</strong>
|
|
204
|
-
</button>
|
|
205
|
-
</li>
|
|
206
|
-
))
|
|
207
|
-
) : (
|
|
208
|
-
<li className="is-empty" aria-disabled>
|
|
209
|
-
{t(language, "brain.graph.search.noMatches")}
|
|
210
|
-
</li>
|
|
211
|
-
)}
|
|
212
|
-
</ul>
|
|
213
|
-
) : null}
|
|
214
|
-
</div>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
<div className="brain-graph-control-panel" role="group" aria-label={t(language, "brain.graph.control.panel")}>
|
|
218
|
-
<div className="brain-graph-filter-row">
|
|
219
|
-
<span className="brain-graph-control-label">
|
|
220
|
-
<Filter className="h-3 w-3" aria-hidden />
|
|
221
|
-
{t(language, "brain.graph.control.types")}
|
|
222
|
-
</span>
|
|
223
|
-
<div className="brain-graph-type-chips" role="group" aria-label={t(language, "brain.graph.control.types")}>
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
className={`brain-graph-chip ${activeTypes.size === 0 ? "is-active" : ""}`}
|
|
227
|
-
aria-pressed={activeTypes.size === 0}
|
|
228
|
-
onClick={() => setActiveTypes(new Set())}
|
|
229
|
-
>
|
|
230
|
-
{t(language, "brain.graph.control.allTypes")}
|
|
231
|
-
</button>
|
|
232
|
-
{availableTypes.map((type) => {
|
|
233
|
-
const active = activeTypes.has(type);
|
|
234
|
-
return (
|
|
235
|
-
<button
|
|
236
|
-
key={type}
|
|
237
|
-
type="button"
|
|
238
|
-
className={`brain-graph-chip ${active ? "is-active" : ""}`}
|
|
239
|
-
aria-pressed={active}
|
|
240
|
-
aria-label={t(language, "brain.graph.control.toggleType", { type })}
|
|
241
|
-
onClick={() =>
|
|
242
|
-
setActiveTypes((prev) => {
|
|
243
|
-
const next = new Set(prev);
|
|
244
|
-
if (next.has(type)) next.delete(type);
|
|
245
|
-
else next.add(type);
|
|
246
|
-
return next;
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
>
|
|
250
|
-
{type}
|
|
251
|
-
</button>
|
|
252
|
-
);
|
|
253
|
-
})}
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
<div className="brain-graph-filter-row">
|
|
258
|
-
<span className="brain-graph-control-label">{t(language, "brain.graph.control.time")}</span>
|
|
259
|
-
<div className="brain-graph-time-chips" role="group" aria-label={t(language, "brain.graph.control.dateRange")}>
|
|
260
|
-
{TIME_WINDOWS.map((window) => {
|
|
261
|
-
const active = timeDays === window.days;
|
|
262
|
-
const label =
|
|
263
|
-
window.days === null
|
|
264
|
-
? t(language, "brain.graph.control.allTime")
|
|
265
|
-
: t(language, "brain.graph.control.recentWindow", { days: window.days });
|
|
266
|
-
return (
|
|
267
|
-
<button
|
|
268
|
-
key={window.days ?? "all"}
|
|
269
|
-
type="button"
|
|
270
|
-
className={`brain-graph-chip ${active ? "is-active" : ""}`}
|
|
271
|
-
aria-pressed={active}
|
|
272
|
-
disabled={window.days !== null && !hasTimestamps}
|
|
273
|
-
onClick={() => setTimeDays(window.days)}
|
|
274
|
-
>
|
|
275
|
-
{label}
|
|
276
|
-
</button>
|
|
277
|
-
);
|
|
278
|
-
})}
|
|
279
|
-
</div>
|
|
280
|
-
{filtersActive ? (
|
|
281
|
-
<button
|
|
282
|
-
type="button"
|
|
283
|
-
className="brain-graph-reset"
|
|
284
|
-
onClick={() => {
|
|
285
|
-
setActiveTypes(new Set());
|
|
286
|
-
setTimeDays(null);
|
|
287
|
-
}}
|
|
288
|
-
>
|
|
289
|
-
<RotateCcw className="h-3 w-3" aria-hidden />
|
|
290
|
-
{t(language, "brain.graph.control.resetFilters")}
|
|
291
|
-
</button>
|
|
292
|
-
) : null}
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
|
|
296
|
-
{visibleNodes.length ? (
|
|
297
|
-
<div className="brain-graph-canvas" data-focus-active={focusActive ? "true" : "false"}>
|
|
298
|
-
<svg className="brain-graph-edges" viewBox="0 0 100 100" aria-hidden>
|
|
299
|
-
{visibleEdges.map((edge, index) => {
|
|
300
|
-
const source = positionById.get(edge.source);
|
|
301
|
-
const target = positionById.get(edge.target);
|
|
302
|
-
if (!source || !target) return null;
|
|
303
|
-
const touchesFocus =
|
|
304
|
-
focusActive && (edge.source === selectedId || edge.target === selectedId);
|
|
305
|
-
const touchesMatch = matchedIds.has(edge.source) || matchedIds.has(edge.target);
|
|
306
|
-
const highlight = touchesFocus || (query ? touchesMatch : false);
|
|
307
|
-
return (
|
|
308
|
-
<line
|
|
309
|
-
key={`${edge.id}-${index}`}
|
|
310
|
-
x1={source.x}
|
|
311
|
-
y1={source.y}
|
|
312
|
-
x2={target.x}
|
|
313
|
-
y2={target.y}
|
|
314
|
-
data-highlight={
|
|
315
|
-
focusActive || query ? (highlight ? "true" : "false") : undefined
|
|
316
|
-
}
|
|
317
|
-
style={{ "--weight": String(clamp(edge.weight, 0.4, 2.8)) } as React.CSSProperties}
|
|
318
|
-
/>
|
|
319
|
-
);
|
|
320
|
-
})}
|
|
321
|
-
</svg>
|
|
322
|
-
{layout.map(({ node, x, y }, index) => {
|
|
323
|
-
const isRecent =
|
|
324
|
-
typeof node.createdAt === "number" && Date.now() - node.createdAt <= RECENT_WINDOW_MS;
|
|
325
|
-
return (
|
|
326
|
-
<button
|
|
327
|
-
key={node.id}
|
|
328
|
-
type="button"
|
|
329
|
-
className={`graph-node ${selected?.id === node.id ? "is-selected" : ""} ${matchedIds.has(node.id) ? "is-match" : ""}`}
|
|
330
|
-
data-focus={focusState(node.id)}
|
|
331
|
-
data-recent={isRecent ? "true" : undefined}
|
|
332
|
-
style={layerStyle({ "--x": `${x}%`, "--y": `${y}%`, "--delay": `${index * 35}ms` })}
|
|
333
|
-
onClick={() => onSelect(selectedId === node.id ? null : node.id)}
|
|
334
|
-
>
|
|
335
|
-
<span>{node.type}</span>
|
|
336
|
-
{highlightMatch(node.label, query)}
|
|
337
|
-
</button>
|
|
338
|
-
);
|
|
339
|
-
})}
|
|
340
|
-
</div>
|
|
341
|
-
) : (
|
|
342
|
-
<div className="brain-graph-empty">{t(language, "brain.graph.empty")}</div>
|
|
343
|
-
)}
|
|
344
|
-
|
|
345
|
-
<div className="brain-graph-focus">
|
|
346
|
-
{selected ? (
|
|
347
|
-
<>
|
|
348
|
-
<span>{selected.type}</span>
|
|
349
|
-
<strong>{selected.label}</strong>
|
|
350
|
-
<p>{selected.summary || t(language, "brain.graph.summaryFallback")}</p>
|
|
351
|
-
{focusActive ? (
|
|
352
|
-
<p className="brain-graph-focus-meta" role="status">
|
|
353
|
-
{t(language, "brain.graph.focus.neighbors", { count: neighborIds.size })}
|
|
354
|
-
</p>
|
|
355
|
-
) : (
|
|
356
|
-
<p>{t(language, "brain.graph.focused")}</p>
|
|
357
|
-
)}
|
|
358
|
-
</>
|
|
359
|
-
) : (
|
|
360
|
-
<p>{t(language, "brain.graph.emptyFocus")}</p>
|
|
361
|
-
)}
|
|
362
|
-
</div>
|
|
363
|
-
</section>
|
|
364
|
-
);
|
|
365
|
-
}
|