ltcai 8.0.0 → 8.2.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 (183) hide show
  1. package/README.md +33 -72
  2. package/docs/CHANGELOG.md +52 -874
  3. package/docs/DEVELOPMENT.md +26 -3
  4. package/docs/LEGACY_COMPATIBILITY.md +3 -1
  5. package/docs/TRUST_MODEL.md +26 -26
  6. package/docs/WHY_LATTICE.md +28 -25
  7. package/docs/kg-schema.md +6 -4
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/embeddings.py +9 -1
  10. package/lattice_brain/graph/schema.py +6 -0
  11. package/lattice_brain/runtime/multi_agent.py +1 -1
  12. package/latticeai/__init__.py +1 -1
  13. package/latticeai/api/computer_use.py +2 -0
  14. package/latticeai/api/memory.py +13 -0
  15. package/latticeai/app_factory.py +43 -162
  16. package/latticeai/brain/__init__.py +1 -1
  17. package/latticeai/core/config.py +10 -0
  18. package/latticeai/core/local_embeddings.py +8 -0
  19. package/latticeai/core/marketplace.py +1 -1
  20. package/latticeai/core/mcp_registry.py +140 -1
  21. package/latticeai/core/tool_registry.py +10 -0
  22. package/latticeai/core/workspace_graph_trace.py +132 -0
  23. package/latticeai/core/workspace_memory.py +75 -0
  24. package/latticeai/core/workspace_os.py +135 -1283
  25. package/latticeai/core/workspace_os_utils.py +132 -0
  26. package/latticeai/core/workspace_permissions.py +99 -0
  27. package/latticeai/core/workspace_plugins.py +97 -0
  28. package/latticeai/core/workspace_runs.py +612 -0
  29. package/latticeai/core/workspace_skills.py +114 -0
  30. package/latticeai/core/workspace_snapshots.py +195 -0
  31. package/latticeai/core/workspace_timeline.py +107 -0
  32. package/latticeai/runtime/audit_runtime.py +64 -0
  33. package/latticeai/runtime/config_runtime.py +4 -0
  34. package/latticeai/runtime/sso_runtime.py +52 -0
  35. package/latticeai/services/architecture_readiness.py +6 -5
  36. package/latticeai/services/memory_service.py +215 -7
  37. package/latticeai/services/model_capability_registry.py +68 -0
  38. package/latticeai/services/model_engines.py +603 -0
  39. package/latticeai/services/model_loading.py +482 -0
  40. package/latticeai/services/model_recommendation.py +5 -0
  41. package/latticeai/services/model_runtime.py +146 -837
  42. package/latticeai/services/product_readiness.py +19 -18
  43. package/package.json +2 -2
  44. package/scripts/build_frontend_assets.mjs +11 -2
  45. package/src-tauri/Cargo.lock +1 -1
  46. package/src-tauri/Cargo.toml +1 -1
  47. package/src-tauri/tauri.conf.json +1 -1
  48. package/static/app/asset-manifest.json +11 -102
  49. package/static/app/assets/Act-D9jIknFd.js +1 -0
  50. package/static/app/assets/Brain-CFOtWbPN.js +321 -0
  51. package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
  52. package/static/app/assets/Library-C5Q2yWee.js +1 -0
  53. package/static/app/assets/System-BLbjdr1_.js +1 -0
  54. package/static/app/assets/core-CwxXejkd.js +1 -2
  55. package/static/app/assets/index-BqammyNu.js +16 -0
  56. package/static/app/assets/index-ty1iGgZu.css +2 -0
  57. package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
  58. package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.js} +1 -2
  59. package/static/app/index.html +3 -10
  60. package/static/app/theme-boot.js +8 -0
  61. package/static/sw.js +1 -1
  62. package/tools/__init__.py +2 -1
  63. package/tools/computer.py +21 -0
  64. package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
  65. package/docs/HANDOVER_v3.6.0.md +0 -46
  66. package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
  67. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
  68. package/docs/V2_ARCHITECTURE.md +0 -561
  69. package/docs/V3_2_AUDIT.md +0 -82
  70. package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
  71. package/docs/V3_FRONTEND.md +0 -140
  72. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
  73. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
  74. package/docs/V4_1_VALIDATION_REPORT.md +0 -47
  75. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
  76. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
  77. package/docs/V4_2_VALIDATION_REPORT.md +0 -89
  78. package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
  79. package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
  80. package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
  81. package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
  82. package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
  83. package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
  84. package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
  85. package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
  86. package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
  87. package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
  88. package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
  89. package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
  90. package/docs/V4_3_VALIDATION_REPORT.md +0 -58
  91. package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
  92. package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
  93. package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
  94. package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
  95. package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
  96. package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
  97. package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
  98. package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
  99. package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
  100. package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
  101. package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
  102. package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
  103. package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
  104. package/docs/V4_5_1_UX_REPORT.md +0 -45
  105. package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
  106. package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
  107. package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
  108. package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
  109. package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
  110. package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
  111. package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
  112. package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
  113. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
  114. package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
  115. package/frontend/index.html +0 -24
  116. package/frontend/openapi.json +0 -15425
  117. package/frontend/src/App.tsx +0 -243
  118. package/frontend/src/api/client.ts +0 -580
  119. package/frontend/src/api/openapi.ts +0 -17892
  120. package/frontend/src/components/AdminAccessGate.tsx +0 -70
  121. package/frontend/src/components/FeedbackState.tsx +0 -45
  122. package/frontend/src/components/LanguageSwitcher.tsx +0 -23
  123. package/frontend/src/components/LivingBrain.tsx +0 -220
  124. package/frontend/src/components/ProductFlow.tsx +0 -171
  125. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +0 -176
  126. package/frontend/src/components/onboarding/AnalysisScreen.tsx +0 -135
  127. package/frontend/src/components/onboarding/DownloadConsentPanel.tsx +0 -29
  128. package/frontend/src/components/onboarding/InstallScreen.tsx +0 -263
  129. package/frontend/src/components/onboarding/LanguageChooser.tsx +0 -23
  130. package/frontend/src/components/onboarding/LoginScreen.tsx +0 -131
  131. package/frontend/src/components/onboarding/ProductFlowScreens.tsx +0 -13
  132. package/frontend/src/components/onboarding/RecommendationScreen.tsx +0 -116
  133. package/frontend/src/components/onboarding/recommendationModel.ts +0 -189
  134. package/frontend/src/components/primitives.tsx +0 -392
  135. package/frontend/src/components/ui/badge.tsx +0 -27
  136. package/frontend/src/components/ui/button.tsx +0 -37
  137. package/frontend/src/components/ui/card.tsx +0 -22
  138. package/frontend/src/components/ui/input.tsx +0 -16
  139. package/frontend/src/components/ui/textarea.tsx +0 -16
  140. package/frontend/src/features/admin/AdminConsole.tsx +0 -334
  141. package/frontend/src/features/brain/BrainCarePanel.tsx +0 -254
  142. package/frontend/src/features/brain/BrainComposer.tsx +0 -85
  143. package/frontend/src/features/brain/BrainConversation.tsx +0 -688
  144. package/frontend/src/features/brain/BrainGraphLayer.tsx +0 -365
  145. package/frontend/src/features/brain/BrainHome.tsx +0 -624
  146. package/frontend/src/features/brain/BrainMemoryLayer.tsx +0 -45
  147. package/frontend/src/features/brain/BrainOverviewPanel.tsx +0 -149
  148. package/frontend/src/features/brain/BrainRelationshipLayer.tsx +0 -43
  149. package/frontend/src/features/brain/DepthEmergence.tsx +0 -53
  150. package/frontend/src/features/brain/brainData.ts +0 -246
  151. package/frontend/src/features/brain/graphLayout.ts +0 -37
  152. package/frontend/src/features/brain/types.ts +0 -150
  153. package/frontend/src/features/review/ReviewCard.tsx +0 -100
  154. package/frontend/src/features/review/ReviewInbox.tsx +0 -127
  155. package/frontend/src/features/review/reviewHelpers.ts +0 -69
  156. package/frontend/src/i18n.ts +0 -1303
  157. package/frontend/src/lib/utils.ts +0 -33
  158. package/frontend/src/main.tsx +0 -23
  159. package/frontend/src/pages/Act.tsx +0 -458
  160. package/frontend/src/pages/Ask.tsx +0 -14
  161. package/frontend/src/pages/Brain.tsx +0 -914
  162. package/frontend/src/pages/Capture.tsx +0 -258
  163. package/frontend/src/pages/Library.tsx +0 -486
  164. package/frontend/src/pages/System.tsx +0 -621
  165. package/frontend/src/routes.ts +0 -92
  166. package/frontend/src/store/appStore.ts +0 -94
  167. package/frontend/src/styles.css +0 -6579
  168. package/static/app/assets/Act-DOvf59ru.js +0 -2
  169. package/static/app/assets/Act-DOvf59ru.js.map +0 -1
  170. package/static/app/assets/Brain-C7_0mEiI.js +0 -322
  171. package/static/app/assets/Brain-C7_0mEiI.js.map +0 -1
  172. package/static/app/assets/Capture-B9Tlhzqr.js.map +0 -1
  173. package/static/app/assets/Library-BJPEEm5O.js +0 -2
  174. package/static/app/assets/Library-BJPEEm5O.js.map +0 -1
  175. package/static/app/assets/System-D6t9jo9V.js +0 -2
  176. package/static/app/assets/System-D6t9jo9V.js.map +0 -1
  177. package/static/app/assets/core-CwxXejkd.js.map +0 -1
  178. package/static/app/assets/index-C7g26IF6.css +0 -2
  179. package/static/app/assets/index-DbcEYJQ2.js +0 -17
  180. package/static/app/assets/index-DbcEYJQ2.js.map +0 -1
  181. package/static/app/assets/primitives-CD38lt4n.js +0 -2
  182. package/static/app/assets/primitives-CD38lt4n.js.map +0 -1
  183. 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
- }