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,914 +0,0 @@
1
- import * as React from "react";
2
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3
- import cytoscape, { Core, ElementDefinition } from "cytoscape";
4
- import { BrainCircuit, DatabaseBackup, Filter, Focus, Layers3, LocateFixed, Search, Sparkles } from "lucide-react";
5
- import { latticeApi } from "@/api/client";
6
- import { type BrainState } from "@/components/LivingBrain";
7
- import { ActionButton, DataPanel, EmptyState, EntityList, KeyValueList, LoadingPanel, OperationResult, StatGrid, StructuredView, Tabs } from "@/components/primitives";
8
- import { Badge } from "@/components/ui/badge";
9
- import { Button } from "@/components/ui/button";
10
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
11
- import { Input } from "@/components/ui/input";
12
- import { Textarea } from "@/components/ui/textarea";
13
- import { BrainHome } from "@/features/brain/BrainHome";
14
- import { useAppStore } from "@/store/appStore";
15
- import { asArray, fmtNumber, pct, shortId, titleize } from "@/lib/utils";
16
-
17
- type BrainTab = "conversation" | "memory" | "knowledge" | "relationships" | "graph" | "portability";
18
- type LabelMode = "important" | "all" | "off";
19
-
20
- type GraphNode = {
21
- id: string;
22
- label: string;
23
- type: string;
24
- group: string;
25
- summary: string;
26
- source: string;
27
- importance: number;
28
- degree: number;
29
- searchText: string;
30
- raw: Record<string, unknown>;
31
- };
32
-
33
- type GraphEdge = {
34
- id: string;
35
- source: string;
36
- target: string;
37
- label: string;
38
- weight: number;
39
- };
40
-
41
- type GraphGroup = {
42
- id: string;
43
- label: string;
44
- color: string;
45
- count: number;
46
- visibleCount: number;
47
- collapsed: boolean;
48
- };
49
-
50
- type ParsedGraph = {
51
- nodes: GraphNode[];
52
- edges: GraphEdge[];
53
- groups: GraphGroup[];
54
- };
55
-
56
- type ExplorerModel = ParsedGraph & {
57
- elements: ElementDefinition[];
58
- visibleNodes: GraphNode[];
59
- visibleEdges: GraphEdge[];
60
- totalNodes: number;
61
- totalEdges: number;
62
- hiddenByFilters: number;
63
- };
64
-
65
- const tabs: Array<{ id: BrainTab; label: string }> = [
66
- { id: "conversation", label: "Brain" },
67
- { id: "memory", label: "Memories" },
68
- { id: "knowledge", label: "Knowledge" },
69
- { id: "relationships", label: "Relationships" },
70
- { id: "graph", label: "Graph" },
71
- { id: "portability", label: "Care" },
72
- ];
73
-
74
- const groupDefinitions = [
75
- { id: "knowledge", label: "Knowledge", color: "#20c997", types: ["topic", "concept", "entity", "decision", "insight", "claim", "fact"] },
76
- { id: "source", label: "Sources", color: "#60a5fa", types: ["file", "document", "source", "chunk", "note", "url", "page", "image", "transcript"] },
77
- { id: "activity", label: "Activity", color: "#f59e0b", types: ["task", "workflow", "agent", "run", "approval", "hook"] },
78
- { id: "memory", label: "Memory", color: "#a78bfa", types: ["memory", "conversation", "message", "chat", "context"] },
79
- { id: "people", label: "People", color: "#f472b6", types: ["person", "user", "team", "organization", "org"] },
80
- { id: "system", label: "System", color: "#94a3b8", types: ["model", "skill", "plugin", "setting", "policy", "device", "storage"] },
81
- { id: "other", label: "Other", color: "#f8fafc", types: [] },
82
- ] as const;
83
-
84
- const groupLookup: Map<string, string> = new Map(groupDefinitions.flatMap((group) => group.types.map((type) => [type, group.id])));
85
-
86
- function isRecord(value: unknown): value is Record<string, unknown> {
87
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
88
- }
89
-
90
- function field(record: Record<string, unknown>, keys: string[], fallback = "") {
91
- for (const key of keys) {
92
- const value = record[key];
93
- if (typeof value === "string" && value.trim()) return value;
94
- if (typeof value === "number" && Number.isFinite(value)) return String(value);
95
- }
96
- return fallback;
97
- }
98
-
99
- function numberField(record: Record<string, unknown>, keys: string[]) {
100
- for (const key of keys) {
101
- const value = Number(record[key]);
102
- if (Number.isFinite(value)) return value;
103
- }
104
- return 0;
105
- }
106
-
107
- function nestedRecord(record: Record<string, unknown>, key: string) {
108
- return isRecord(record[key]) ? record[key] as Record<string, unknown> : {};
109
- }
110
-
111
- function groupForType(type: string) {
112
- return groupLookup.get(type.toLowerCase()) || "other";
113
- }
114
-
115
- function groupDefinition(id: string) {
116
- return groupDefinitions.find((group) => group.id === id) || groupDefinitions[groupDefinitions.length - 1];
117
- }
118
-
119
- function parseGraph(data: unknown): ParsedGraph {
120
- const graph = isRecord(data) ? data : {};
121
- const rawNodes = asArray<Record<string, unknown>>(graph.nodes);
122
- const rawEdges = asArray<Record<string, unknown>>(graph.edges);
123
- const ids = new Set(rawNodes.map((node) => field(node, ["id", "node_id", "title", "label"])).filter(Boolean));
124
- const edges = rawEdges.flatMap((edge, index): GraphEdge[] => {
125
- const source = field(edge, ["from", "source", "source_id"]);
126
- const target = field(edge, ["to", "target", "target_id"]);
127
- if (!source || !target || !ids.has(source) || !ids.has(target)) return [];
128
- return [{
129
- id: field(edge, ["id"], `edge-${index}`),
130
- source,
131
- target,
132
- label: field(edge, ["type", "label", "relationship"], "related"),
133
- weight: Math.max(0.2, numberField(edge, ["weight", "score", "confidence"]) || 1),
134
- }];
135
- });
136
- const degree = new Map<string, number>();
137
- edges.forEach((edge) => {
138
- degree.set(edge.source, (degree.get(edge.source) || 0) + 1);
139
- degree.set(edge.target, (degree.get(edge.target) || 0) + 1);
140
- });
141
- const maxDegree = Math.max(1, ...Array.from(degree.values()));
142
- const nodes = rawNodes.flatMap((node): GraphNode[] => {
143
- const id = field(node, ["id", "node_id", "title", "label"]);
144
- if (!id) return [];
145
- const metadata = nestedRecord(node, "metadata");
146
- const metrics = nestedRecord(metadata, "graph_metrics");
147
- const type = field(node, ["type", "kind", "category"], "Node");
148
- const label = field(node, ["title", "label", "name"], shortId(id, 38));
149
- const explicitImportance = numberField(node, ["importance_norm", "importance", "score"]) || numberField(metrics, ["importance_norm", "importance", "centrality"]);
150
- const nodeDegree = degree.get(id) || 0;
151
- const importance = Math.max(0.08, Math.min(1, explicitImportance || (nodeDegree / maxDegree) * 0.8 + 0.12));
152
- const summary = field(node, ["summary", "description", "snippet"]) || field(metadata, ["summary", "description", "relative_path", "filename"]);
153
- const source = field(node, ["source", "path"]) || field(metadata, ["source", "relative_path", "filename"]);
154
- const searchText = [id, label, type, summary, source, Object.keys(metadata).join(" ")].join(" ").toLowerCase();
155
- return [{ id, label, type, group: groupForType(type), summary, source, importance, degree: nodeDegree, searchText, raw: node }];
156
- });
157
- const groupCounts = new Map<string, number>();
158
- nodes.forEach((node) => groupCounts.set(node.group, (groupCounts.get(node.group) || 0) + 1));
159
- const groups = groupDefinitions.map((group) => ({
160
- id: group.id,
161
- label: group.label,
162
- color: group.color,
163
- count: groupCounts.get(group.id) || 0,
164
- visibleCount: 0,
165
- collapsed: false,
166
- })).filter((group) => group.count > 0);
167
- return { nodes, edges, groups };
168
- }
169
-
170
- function buildExplorerModel({
171
- graph,
172
- search,
173
- groupFilter,
174
- minImportance,
175
- collapsedGroups,
176
- selectedId,
177
- labelMode,
178
- maxNodes,
179
- }: {
180
- graph: ParsedGraph;
181
- search: string;
182
- groupFilter: string;
183
- minImportance: number;
184
- collapsedGroups: Set<string>;
185
- selectedId: string | null;
186
- labelMode: LabelMode;
187
- maxNodes: number;
188
- }): ExplorerModel {
189
- const query = search.trim().toLowerCase();
190
- const neighborIds = new Set<string>();
191
- if (selectedId && !selectedId.startsWith("group:")) {
192
- neighborIds.add(selectedId);
193
- graph.edges.forEach((edge) => {
194
- if (edge.source === selectedId) neighborIds.add(edge.target);
195
- if (edge.target === selectedId) neighborIds.add(edge.source);
196
- });
197
- }
198
- const filtered = graph.nodes
199
- .filter((node) => groupFilter === "all" || node.group === groupFilter || node.type === groupFilter)
200
- .filter((node) => query ? node.searchText.includes(query) : node.importance >= minImportance)
201
- .filter((node) => !neighborIds.size || neighborIds.has(node.id))
202
- .sort((a, b) => (b.importance + b.degree / 25) - (a.importance + a.degree / 25));
203
- const capped = filtered.slice(0, maxNodes);
204
- const visibleCandidateIds = new Set(capped.map((node) => node.id));
205
- const aggregateNodes = new Map<string, { id: string; group: GraphGroup; count: number; maxImportance: number }>();
206
- const visibleNodes = capped.filter((node) => {
207
- if (!collapsedGroups.has(node.group)) return true;
208
- const definition = groupDefinition(node.group);
209
- const aggregateId = `group:${node.group}`;
210
- const current = aggregateNodes.get(aggregateId);
211
- aggregateNodes.set(aggregateId, {
212
- id: aggregateId,
213
- group: { id: definition.id, label: definition.label, color: definition.color, count: 0, visibleCount: 0, collapsed: true },
214
- count: (current?.count || 0) + 1,
215
- maxImportance: Math.max(current?.maxImportance || 0, node.importance),
216
- });
217
- return false;
218
- });
219
- const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
220
- const aggregateIds = new Set(aggregateNodes.keys());
221
- const visibleEdges = graph.edges.filter((edge) => visibleCandidateIds.has(edge.source) && visibleCandidateIds.has(edge.target));
222
- const mappedEdges = visibleEdges.flatMap((edge, index): ElementDefinition[] => {
223
- const sourceNode = graph.nodes.find((node) => node.id === edge.source);
224
- const targetNode = graph.nodes.find((node) => node.id === edge.target);
225
- const source = visibleNodeIds.has(edge.source) ? edge.source : sourceNode && aggregateIds.has(`group:${sourceNode.group}`) ? `group:${sourceNode.group}` : "";
226
- const target = visibleNodeIds.has(edge.target) ? edge.target : targetNode && aggregateIds.has(`group:${targetNode.group}`) ? `group:${targetNode.group}` : "";
227
- if (!source || !target || source === target) return [];
228
- return [{
229
- data: {
230
- id: `${edge.id}-${index}-${source}-${target}`,
231
- source,
232
- target,
233
- label: edge.label,
234
- width: Math.max(1, Math.min(4, edge.weight)),
235
- },
236
- classes: selectedId && (edge.source === selectedId || edge.target === selectedId) ? "connected" : "",
237
- }];
238
- });
239
- const nodeElements: ElementDefinition[] = visibleNodes.map((node) => {
240
- const definition = groupDefinition(node.group);
241
- const matched = query && node.searchText.includes(query);
242
- const label = labelMode === "off" ? "" : labelMode === "all" || node.importance > 0.55 || node.degree > 1 || matched || selectedId === node.id ? node.label : "";
243
- return {
244
- data: {
245
- id: node.id,
246
- label: node.label,
247
- displayLabel: label,
248
- type: node.type,
249
- group: definition.label,
250
- color: definition.color,
251
- borderColor: selectedId === node.id ? "#ffffff" : definition.color,
252
- size: Math.round(20 + node.importance * 34 + Math.min(node.degree, 10) * 2),
253
- },
254
- classes: [
255
- selectedId === node.id ? "selected" : "",
256
- matched ? "match" : "",
257
- neighborIds.size && !neighborIds.has(node.id) ? "faded" : "",
258
- ].filter(Boolean).join(" "),
259
- };
260
- });
261
- const aggregateElements: ElementDefinition[] = Array.from(aggregateNodes.values()).map((aggregate) => ({
262
- data: {
263
- id: aggregate.id,
264
- label: aggregate.group.label,
265
- displayLabel: `${aggregate.group.label} (${aggregate.count})`,
266
- type: "Cluster",
267
- group: aggregate.group.label,
268
- color: aggregate.group.color,
269
- borderColor: "#ffffff",
270
- size: Math.round(34 + Math.min(aggregate.count, 28) * 2 + aggregate.maxImportance * 16),
271
- },
272
- classes: "cluster",
273
- }));
274
- const visibleCounts = new Map<string, number>();
275
- visibleNodes.forEach((node) => visibleCounts.set(node.group, (visibleCounts.get(node.group) || 0) + 1));
276
- const groups = graph.groups.map((group) => ({
277
- ...group,
278
- visibleCount: (visibleCounts.get(group.id) || 0) + (aggregateNodes.get(`group:${group.id}`)?.count || 0),
279
- collapsed: collapsedGroups.has(group.id),
280
- }));
281
- return {
282
- ...graph,
283
- groups,
284
- elements: [...aggregateElements, ...nodeElements, ...mappedEdges],
285
- visibleNodes,
286
- visibleEdges,
287
- totalNodes: graph.nodes.length,
288
- totalEdges: graph.edges.length,
289
- hiddenByFilters: Math.max(0, graph.nodes.length - capped.length),
290
- };
291
- }
292
-
293
- function CytoscapeGraph({
294
- model,
295
- selectedId,
296
- onSelect,
297
- fitSignal,
298
- }: {
299
- model: ExplorerModel;
300
- selectedId: string | null;
301
- onSelect: (id: string | null) => void;
302
- fitSignal: number;
303
- }) {
304
- const hostRef = React.useRef<HTMLDivElement | null>(null);
305
- const cyRef = React.useRef<Core | null>(null);
306
- React.useEffect(() => {
307
- if (!hostRef.current) return;
308
- cyRef.current?.destroy();
309
- cyRef.current = cytoscape({
310
- container: hostRef.current,
311
- elements: model.elements,
312
- style: [
313
- {
314
- selector: "node",
315
- style: {
316
- "background-color": "data(color)",
317
- "border-color": "data(borderColor)",
318
- "border-width": 1.5,
319
- color: "#f8fafc",
320
- label: "data(displayLabel)",
321
- "font-size": 10,
322
- "font-weight": 600,
323
- "text-outline-color": "#071012",
324
- "text-outline-width": 2.5,
325
- width: "data(size)",
326
- height: "data(size)",
327
- "overlay-opacity": 0,
328
- },
329
- },
330
- {
331
- selector: "node.cluster",
332
- style: {
333
- shape: "round-rectangle",
334
- "background-opacity": 0.46,
335
- "border-width": 2,
336
- "font-size": 12,
337
- "text-valign": "center",
338
- "text-halign": "center",
339
- },
340
- },
341
- {
342
- selector: "node.selected",
343
- style: {
344
- "border-width": 4,
345
- },
346
- },
347
- {
348
- selector: "node.match",
349
- style: {
350
- "border-width": 4,
351
- "border-color": "#fef08a",
352
- },
353
- },
354
- {
355
- selector: "edge",
356
- style: {
357
- width: "data(width)",
358
- "line-color": "#64748b",
359
- "target-arrow-shape": "triangle",
360
- "target-arrow-color": "#64748b",
361
- "curve-style": "bezier",
362
- "arrow-scale": 0.7,
363
- opacity: 0.72,
364
- },
365
- },
366
- {
367
- selector: "edge.connected",
368
- style: {
369
- width: 2.6,
370
- "line-color": "#fef08a",
371
- "target-arrow-color": "#fef08a",
372
- opacity: 1,
373
- },
374
- },
375
- ],
376
- layout: {
377
- name: "cose",
378
- animate: false,
379
- idealEdgeLength: 120,
380
- nodeRepulsion: 5800,
381
- gravity: 0.28,
382
- numIter: 1200,
383
- fit: true,
384
- padding: 32,
385
- },
386
- wheelSensitivity: 0.18,
387
- });
388
- cyRef.current.on("tap", "node", (event) => onSelect(String(event.target.id())));
389
- cyRef.current.on("tap", (event) => {
390
- if (event.target === cyRef.current) onSelect(null);
391
- });
392
- return () => cyRef.current?.destroy();
393
- }, [model.elements, onSelect]);
394
-
395
- React.useEffect(() => {
396
- cyRef.current?.fit(undefined, 32);
397
- }, [fitSignal]);
398
-
399
- React.useEffect(() => {
400
- if (!cyRef.current || !selectedId) return;
401
- const node = cyRef.current.getElementById(selectedId);
402
- if (node.length) {
403
- cyRef.current.animate({ center: { eles: node }, zoom: Math.max(cyRef.current.zoom(), 1.15) }, { duration: 180 });
404
- }
405
- }, [selectedId]);
406
-
407
- return (
408
- <div
409
- ref={hostRef}
410
- data-testid="brain-cytoscape"
411
- className="brain-grid h-[620px] min-h-[32rem] w-full overflow-hidden rounded-lg border border-border bg-background/80"
412
- />
413
- );
414
- }
415
-
416
- export function BrainPage({ initialTab }: { initialTab?: string }) {
417
- const mode = useAppStore((state) => state.mode);
418
- const normalizedInitialTab = normalizeBrainTab(initialTab);
419
- const [tab, setTab] = React.useState<BrainTab>(normalizedInitialTab);
420
- const [brainPresence, setBrainPresence] = React.useState<{ state: BrainState; intensity: number }>({
421
- state: "idle",
422
- intensity: 0.58,
423
- });
424
- React.useEffect(() => {
425
- setTab(normalizeBrainTab(initialTab));
426
- }, [initialTab]);
427
- const setBrain = React.useCallback((state: BrainState, intensity = 0.58) => {
428
- setBrainPresence({ state, intensity });
429
- }, []);
430
- const graph = useQuery({ queryKey: ["graph"], queryFn: latticeApi.graph });
431
- const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
432
- const index = useQuery({ queryKey: ["index"], queryFn: latticeApi.indexStatus });
433
- const coverage = useQuery({ queryKey: ["coverage"], queryFn: latticeApi.graphCoverage });
434
- const provenance = useQuery({ queryKey: ["provenance"], queryFn: () => latticeApi.graphProvenance(50) });
435
- const memory = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
436
-
437
- return (
438
- <div className="space-y-5">
439
- {tab === "conversation" ? null : (
440
- <header className="brain-layer-header">
441
- <div>
442
- <div className="page-kicker"><BrainCircuit className="h-4 w-4" /> {tabLabel(tab)}</div>
443
- <h1>{tabHeadline(tab)}</h1>
444
- </div>
445
- <div className="brain-layer-meter">
446
- <span>Source coverage</span>
447
- <strong>{pct((coverage.data?.data as Record<string, unknown>)?.coverage_ratio)}</strong>
448
- </div>
449
- </header>
450
- )}
451
- <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as BrainTab)} />
452
-
453
- {tab === "conversation" ? (
454
- <BrainHome brainState={brainPresence.state} intensity={brainPresence.intensity} onBrainChange={setBrain} />
455
- ) : null}
456
- {tab === "memory" ? <MemoryPanel /> : null}
457
- {tab === "knowledge" ? <HybridSearch /> : null}
458
- {tab === "relationships" ? (
459
- <div className="grid gap-4 xl:grid-cols-2">
460
- <DataPanel title="Brain activity" result={stats.data}>
461
- {(data) => <GraphStatus data={data as Record<string, unknown>} />}
462
- </DataPanel>
463
- <DataPanel title="Retrieval rhythm" result={index.data}>
464
- {(data) => <RetrievalStatus data={data as Record<string, unknown>} />}
465
- </DataPanel>
466
- <DataPanel title="Memory layers" result={memory.data}>
467
- {(data) => <MemoryStatus data={data as Record<string, unknown>} />}
468
- </DataPanel>
469
- <DataPanel title="Recent sources" result={provenance.data}>
470
- {(data) => <SourceProvenanceList items={(data as Record<string, unknown>).items || data} />}
471
- </DataPanel>
472
- </div>
473
- ) : null}
474
-
475
- {tab === "graph" ? (
476
- graph.isLoading ? <LoadingPanel title="Deep graph" /> : (
477
- <DataPanel title="Advanced relationship graph" description={mode === "basic" ? "Open the deepest layer when you want to inspect the underlying relationships." : "Explore relationships, sources, and graph structure."} result={graph.data}>
478
- {(data) => <DigitalBrainExplorer data={data} />}
479
- </DataPanel>
480
- )
481
- ) : null}
482
- {tab === "portability" ? <PortabilityPanel /> : null}
483
- </div>
484
- );
485
- }
486
-
487
- function normalizeBrainTab(tab?: string): BrainTab {
488
- if (tab === "overview" || tab === "chat" || tab === "ask") return "conversation";
489
- if (tab === "search") return "knowledge";
490
- if (tab === "provenance" || tab === "sources") return "relationships";
491
- return tabs.some((item) => item.id === tab) ? tab as BrainTab : "conversation";
492
- }
493
-
494
- function tabLabel(tab: BrainTab) {
495
- return tabs.find((item) => item.id === tab)?.label || "Brain";
496
- }
497
-
498
- function tabHeadline(tab: BrainTab) {
499
- if (tab === "memory") return "Memories, before mechanics.";
500
- if (tab === "knowledge") return "Knowledge, gathered into recall.";
501
- if (tab === "relationships") return "Relationships, when you need the why.";
502
- if (tab === "graph") return "The graph, intentionally opened.";
503
- if (tab === "portability") return "Care for the Brain.";
504
- return "Talk to your Brain.";
505
- }
506
-
507
- function GraphStatus({ data }: { data: Record<string, unknown> }) {
508
- const mode = useAppStore((state) => state.mode);
509
- const nodeTypes = Object.keys((data.nodes as Record<string, unknown>) || {});
510
- const edgeTypes = Object.keys((data.edges as Record<string, unknown>) || {});
511
- return (
512
- <div className="space-y-3">
513
- <StatGrid stats={[
514
- { label: "Memories", value: data.total_nodes ?? nodeTypes.reduce((sum, key) => sum + Number(((data.nodes as Record<string, unknown>) || {})[key] || 0), 0) },
515
- { label: "Links", value: data.total_edges ?? edgeTypes.reduce((sum, key) => sum + Number(((data.edges as Record<string, unknown>) || {})[key] || 0), 0) },
516
- { label: "Memory kinds", value: nodeTypes.length },
517
- { label: "Link kinds", value: edgeTypes.length },
518
- ]} />
519
- {mode === "basic" ? (
520
- <div className="flex flex-wrap gap-1">
521
- {[...nodeTypes, ...edgeTypes].slice(0, 10).map((item) => <Badge key={item} variant="muted">{titleize(item)}</Badge>)}
522
- </div>
523
- ) : <StructuredView value={{ memory_kinds: nodeTypes, link_kinds: edgeTypes }} />}
524
- </div>
525
- );
526
- }
527
-
528
- function RetrievalStatus({ data }: { data: Record<string, unknown> }) {
529
- const pipelines = isRecord(data.pipelines) ? data.pipelines : {};
530
- const rows = Object.entries(pipelines).map(([name, value]) => ({
531
- name: titleize(name),
532
- status: isRecord(value) ? String(value.state || value.status || "reported") : "reported",
533
- description: isRecord(value) ? Object.entries(value).filter(([key]) => key !== "state" && key !== "status").slice(0, 3).map(([key, item]) => `${titleize(key)}: ${String(item)}`).join(" · ") : String(value),
534
- }));
535
- return rows.length ? <EntityList items={rows} titleKey="name" metaKey="status" /> : <StructuredView value={data} />;
536
- }
537
-
538
- function MemoryStatus({ data }: { data: Record<string, unknown> }) {
539
- const usage = isRecord(data.usage) ? data.usage : {};
540
- const sources = asArray<Record<string, unknown>>(data.sources || data.tiers);
541
- return (
542
- <div className="space-y-3">
543
- <StatGrid stats={[
544
- { label: "Sources", value: usage.sources ?? asArray(data.sources).length },
545
- { label: "Items", value: usage.total_items ?? asArray(data.sources).reduce((sum, item) => sum + Number(isRecord(item) ? item.count || 0 : 0), 0) },
546
- { label: "Bytes", value: usage.total_bytes ?? 0 },
547
- { label: "Health", value: data.health || "reported" },
548
- ]} />
549
- <SourceProvenanceList items={sources} limit={6} />
550
- </div>
551
- );
552
- }
553
-
554
- function DigitalBrainExplorer({ data }: { data: unknown }) {
555
- const mode = useAppStore((state) => state.mode);
556
- const parsed = React.useMemo(() => parseGraph(data), [data]);
557
- const [search, setSearch] = React.useState("");
558
- const [groupFilter, setGroupFilter] = React.useState("all");
559
- const [minImportance, setMinImportance] = React.useState(mode === "basic" ? 0.1 : 0);
560
- const [labelMode, setLabelMode] = React.useState<LabelMode>("important");
561
- const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set());
562
- const [selectedId, setSelectedId] = React.useState<string | null>(null);
563
- const [fitSignal, setFitSignal] = React.useState(0);
564
- const backendSearch = useMutation({ mutationFn: () => latticeApi.hybridSearch(search.trim()) });
565
- const model = React.useMemo(() => buildExplorerModel({
566
- graph: parsed,
567
- search,
568
- groupFilter,
569
- minImportance,
570
- collapsedGroups,
571
- selectedId,
572
- labelMode,
573
- maxNodes: 220,
574
- }), [parsed, search, groupFilter, minImportance, collapsedGroups, selectedId, labelMode]);
575
- const selected = parsed.nodes.find((node) => node.id === selectedId);
576
- const selectedGroup = selectedId?.startsWith("group:") ? groupDefinition(selectedId.replace("group:", "")) : null;
577
- const toggleGroup = (id: string) => {
578
- setCollapsedGroups((current) => {
579
- const next = new Set(current);
580
- if (next.has(id)) next.delete(id);
581
- else next.add(id);
582
- return next;
583
- });
584
- };
585
- React.useEffect(() => {
586
- if (mode === "basic" && minImportance < 0.1) setMinImportance(0.1);
587
- }, [mode, minImportance]);
588
- if (!parsed.nodes.length) {
589
- return (
590
- <EmptyState
591
- title="No relationship records yet"
592
- detail="Capture a document, note, or local folder to create connected memories with sources."
593
- />
594
- );
595
- }
596
- return (
597
- <div className="space-y-4">
598
- <div className="grid gap-3 xl:grid-cols-[1fr_220px_180px_170px]">
599
- <div className="relative">
600
- <Search className="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
601
- <Input className="pl-9" value={search} onChange={(event) => setSearch(event.target.value)} placeholder={mode === "basic" ? "Search ideas, files, people, and notes..." : "Search graph labels, types, provenance..."} />
602
- </div>
603
- <select className="h-9 rounded-md border border-border bg-background px-3 text-sm" value={groupFilter} onChange={(event) => setGroupFilter(event.target.value)}>
604
- <option value="all">All semantic groups</option>
605
- {model.groups.map((group) => <option key={group.id} value={group.id}>{group.label}</option>)}
606
- </select>
607
- <select className="h-9 rounded-md border border-border bg-background px-3 text-sm" value={labelMode} onChange={(event) => setLabelMode(event.target.value as LabelMode)}>
608
- <option value="important">Important labels</option>
609
- <option value="all">All labels</option>
610
- <option value="off">Hide labels</option>
611
- </select>
612
- <Button variant="outline" onClick={() => setFitSignal((value) => value + 1)}><LocateFixed className="h-4 w-4" /> Fit</Button>
613
- </div>
614
- <div className="grid gap-3 lg:grid-cols-[1fr_18rem]">
615
- <Card>
616
- <CardHeader className="flex-row items-start justify-between gap-3">
617
- <div>
618
- <CardTitle className="flex items-center gap-2"><Layers3 className="h-4 w-4" /> Deep graph</CardTitle>
619
- <CardDescription>
620
- Showing {fmtNumber(model.visibleNodes.length)} ideas and {fmtNumber(model.visibleEdges.length)} connections from {fmtNumber(model.totalNodes)} saved items.
621
- </CardDescription>
622
- </div>
623
- <Badge variant={model.hiddenByFilters ? "warning" : "success"}>{model.hiddenByFilters ? `${fmtNumber(model.hiddenByFilters)} filtered` : "all in view"}</Badge>
624
- </CardHeader>
625
- <CardContent className="space-y-3">
626
- <div className="flex flex-wrap items-center gap-2">
627
- <Filter className="h-4 w-4 text-muted-foreground" />
628
- <label className="text-sm text-muted-foreground" htmlFor="importance">Importance</label>
629
- <input
630
- id="importance"
631
- type="range"
632
- min="0"
633
- max="0.9"
634
- step="0.05"
635
- value={minImportance}
636
- onChange={(event) => setMinImportance(Number(event.target.value))}
637
- className="w-44"
638
- aria-label="Minimum relationship importance"
639
- />
640
- <Badge variant="muted">{Math.round(minImportance * 100)}%+</Badge>
641
- {selectedId ? <Button variant="outline" size="sm" onClick={() => setSelectedId(null)}>Clear focus</Button> : null}
642
- {search.trim() ? <Button variant="outline" size="sm" onClick={() => backendSearch.mutate()} disabled={backendSearch.isPending}>Search all memories</Button> : null}
643
- </div>
644
- <div className="flex flex-wrap gap-2">
645
- {model.groups.map((group) => (
646
- <button
647
- key={group.id}
648
- onClick={() => toggleGroup(group.id)}
649
- className="inline-flex items-center gap-2 rounded-md border border-border bg-background px-2.5 py-1.5 text-xs hover:bg-muted"
650
- >
651
- <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: group.color }} />
652
- <span>{group.label}</span>
653
- <Badge variant={group.collapsed ? "warning" : "muted"}>{group.collapsed ? "collapsed" : fmtNumber(group.count)}</Badge>
654
- </button>
655
- ))}
656
- </div>
657
- <CytoscapeGraph model={model} selectedId={selectedId} onSelect={setSelectedId} fitSignal={fitSignal} />
658
- </CardContent>
659
- </Card>
660
- <aside className="space-y-3">
661
- <Card>
662
- <CardHeader>
663
- <CardTitle className="flex items-center gap-2"><Focus className="h-4 w-4" /> Focus</CardTitle>
664
- <CardDescription>Click any idea to see why it matters.</CardDescription>
665
- </CardHeader>
666
- <CardContent className="space-y-3">
667
- {selected ? (
668
- <>
669
- <div>
670
- <div className="text-lg font-semibold">{selected.label}</div>
671
- <div className="mt-1 flex flex-wrap gap-1">
672
- <Badge variant="muted">{selected.type}</Badge>
673
- <Badge variant="muted">{groupDefinition(selected.group).label}</Badge>
674
- <Badge variant="success">{Math.round(selected.importance * 100)} importance</Badge>
675
- </div>
676
- </div>
677
- {selected.summary ? <p className="text-sm text-muted-foreground">{selected.summary}</p> : null}
678
- {mode === "basic" ? (
679
- <KeyValueList data={{
680
- connections: selected.degree,
681
- source: selected.source || "Source not recorded",
682
- source_type: sourceType(selected.raw),
683
- created_at: sourceCreatedAt(selected.raw) || "Created time not recorded",
684
- }} />
685
- ) : (
686
- <StructuredView value={{
687
- id: selected.id,
688
- degree: selected.degree,
689
- source: selected.source || "Source not recorded",
690
- source_type: sourceType(selected.raw),
691
- created_at: sourceCreatedAt(selected.raw) || "Created time not recorded",
692
- }} />
693
- )}
694
- {selected.source ? <Button variant="outline" size="sm" onClick={() => void navigator.clipboard?.writeText(selected.source)}>Copy source</Button> : null}
695
- </>
696
- ) : selectedGroup ? (
697
- <div className="space-y-2">
698
- <Badge variant="warning">Collapsed group</Badge>
699
- <div className="text-lg font-semibold">{selectedGroup.label}</div>
700
- <Button variant="outline" onClick={() => toggleGroup(selectedGroup.id)}>Expand group</Button>
701
- </div>
702
- ) : <EmptyState title="Nothing selected" detail="Select an item or collapsed group in the graph." />}
703
- </CardContent>
704
- </Card>
705
- <Card>
706
- <CardHeader>
707
- <CardTitle>Important ideas</CardTitle>
708
- <CardDescription>What Lattice thinks is most connected right now.</CardDescription>
709
- </CardHeader>
710
- <CardContent className="space-y-2">
711
- {model.visibleNodes.slice(0, 8).map((node) => (
712
- <button
713
- key={node.id}
714
- onClick={() => setSelectedId(node.id)}
715
- className="block w-full rounded-md border border-border bg-background p-2 text-left text-sm hover:bg-muted"
716
- >
717
- <div className="flex items-center justify-between gap-2">
718
- <span className="font-medium">{node.label}</span>
719
- <Badge variant="muted">{node.type}</Badge>
720
- </div>
721
- <div className="mt-1 text-xs text-muted-foreground">{Math.round(node.importance * 100)} importance · {fmtNumber(node.degree)} links</div>
722
- </button>
723
- ))}
724
- </CardContent>
725
- </Card>
726
- </aside>
727
- </div>
728
- {backendSearch.data ? (
729
- <DataPanel title="Brain search results" result={backendSearch.data}>
730
- {(result) => <EntityList items={(result as Record<string, unknown>).matches || result} titleKey="title" metaKey="type" limit={8} />}
731
- </DataPanel>
732
- ) : null}
733
- </div>
734
- );
735
- }
736
-
737
- function HybridSearch() {
738
- const [query, setQuery] = React.useState("");
739
- const search = useMutation({ mutationFn: () => latticeApi.hybridSearch(query) });
740
- return (
741
- <Card>
742
- <CardHeader>
743
- <CardTitle className="flex items-center gap-2"><Search className="h-4 w-4" /> Brain search</CardTitle>
744
- <CardDescription>Find ideas across memories, documents, and connections.</CardDescription>
745
- </CardHeader>
746
- <CardContent className="space-y-3">
747
- <div className="flex flex-col gap-2 sm:flex-row">
748
- <Input placeholder="Search memories, indexed documents, and relationships" value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && search.mutate()} />
749
- <Button onClick={() => search.mutate()} disabled={!query.trim() || search.isPending}>Search</Button>
750
- </div>
751
- {search.data ? (
752
- <DataPanel title="Results" result={search.data}>
753
- {(data) => <EntityList items={(data as Record<string, unknown>).matches || data} titleKey="title" metaKey="type" limit={12} />}
754
- </DataPanel>
755
- ) : null}
756
- </CardContent>
757
- </Card>
758
- );
759
- }
760
-
761
- function MemoryPanel() {
762
- const [query, setQuery] = React.useState("");
763
- const manager = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
764
- const recall = useMutation({ mutationFn: () => latticeApi.memoryRecall(query, 25) });
765
- return (
766
- <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
767
- <DataPanel title="Memory manager" result={manager.data}>
768
- {(data) => <MemoryStatus data={data as Record<string, unknown>} />}
769
- </DataPanel>
770
- <Card>
771
- <CardHeader>
772
- <CardTitle className="flex items-center gap-2"><Sparkles className="h-4 w-4" /> Recall</CardTitle>
773
- <CardDescription>Bring back related memories from your workspace.</CardDescription>
774
- </CardHeader>
775
- <CardContent className="space-y-3">
776
- <Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Recall memories about..." />
777
- <div className="flex flex-wrap gap-2">
778
- <Button disabled={!query.trim() || recall.isPending} onClick={() => recall.mutate()}>Recall</Button>
779
- <ActionButton label="Compact" action={() => latticeApi.memoryCompact()} />
780
- <ActionButton label="Rebuild vector" action={() => latticeApi.memoryRebuild()} />
781
- </div>
782
- {recall.data ? <OperationResult result={recall.data} successLabel="Recall completed" /> : null}
783
- </CardContent>
784
- </Card>
785
- </div>
786
- );
787
- }
788
-
789
- function ProvenancePanel() {
790
- const provenance = useQuery({ queryKey: ["provenance"], queryFn: () => latticeApi.graphProvenance(80) });
791
- const coverage = useQuery({ queryKey: ["coverage"], queryFn: latticeApi.graphCoverage });
792
- return (
793
- <div className="grid gap-4 xl:grid-cols-[0.8fr_1.2fr]">
794
- <DataPanel title="Coverage" result={coverage.data}>
795
- {(data) => <StructuredView value={data} />}
796
- </DataPanel>
797
- <DataPanel title="Recent sources" result={provenance.data}>
798
- {(data) => <SourceProvenanceList items={(data as Record<string, unknown>).items || data} limit={14} />}
799
- </DataPanel>
800
- </div>
801
- );
802
- }
803
-
804
- function SourceProvenanceList({ items, limit = 8 }: { items: unknown; limit?: number }) {
805
- const rows = asArray<Record<string, unknown>>(items).slice(0, limit);
806
- if (!rows.length) return <EmptyState title="No sources yet" detail="New memories will show their chat, manual, document, or import origin here." />;
807
- return (
808
- <div className="grid gap-2">
809
- {rows.map((item, index) => {
810
- const title = String(item.title || item.label || item.source_title || item.filename || item.path || item.source || `Source ${index + 1}`);
811
- const path = String(item.path || item.source_path || item.source || item.conversation_id || "");
812
- const type = sourceType(item);
813
- const created = sourceCreatedAt(item);
814
- return (
815
- <div key={String(item.id || item.source_id || title)} className="rounded-lg border border-border bg-background/55 p-3">
816
- <div className="flex flex-wrap items-start justify-between gap-2">
817
- <div>
818
- <div className="font-medium">{title}</div>
819
- <div className="mt-1 text-xs text-muted-foreground">
820
- {type} · {created || "Created time not recorded"}
821
- </div>
822
- </div>
823
- <Badge variant="muted">{path ? "inspectable" : "missing provenance"}</Badge>
824
- </div>
825
- {path ? (
826
- <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
827
- <span className="break-all">{path}</span>
828
- <Button variant="outline" size="sm" onClick={() => void navigator.clipboard?.writeText(path)}>Copy source</Button>
829
- </div>
830
- ) : (
831
- <p className="mt-2 text-xs text-muted-foreground">This older memory did not record a source path or conversation. It remains searchable, but provenance is incomplete.</p>
832
- )}
833
- </div>
834
- );
835
- })}
836
- </div>
837
- );
838
- }
839
-
840
- function sourceType(item: Record<string, unknown>) {
841
- const metadata = isRecord(item.metadata) ? item.metadata : {};
842
- const raw = String(item.source_type || item.type || item.kind || metadata.source_type || metadata.role || "").toLowerCase();
843
- if (/chat|conversation|message/.test(raw)) return "chat";
844
- if (/document|upload|file|pdf|markdown|text/.test(raw)) return "document";
845
- if (/import|archive|restore/.test(raw)) return "import";
846
- if (/manual|note/.test(raw)) return "manual";
847
- return "source unknown";
848
- }
849
-
850
- function sourceCreatedAt(item: Record<string, unknown>) {
851
- const metadata = isRecord(item.metadata) ? item.metadata : {};
852
- const value = item.created_at || item.timestamp || item.updated_at || metadata.created_at || metadata.timestamp;
853
- return value ? String(value) : "";
854
- }
855
-
856
- function PortabilityPanel() {
857
- const qc = useQueryClient();
858
- const [artifact, setArtifact] = React.useState("");
859
- const port = useQuery({ queryKey: ["portability"], queryFn: latticeApi.graphPortability });
860
- const importMutation = useMutation({
861
- mutationFn: async () => {
862
- try {
863
- return await latticeApi.graphImport(JSON.parse(artifact), true);
864
- } catch (err) {
865
- return { ok: false, status: 0, data: {}, source: "unavailable" as const, error: err instanceof Error ? err.message : String(err) };
866
- }
867
- },
868
- onSuccess: () => qc.invalidateQueries({ queryKey: ["portability"] }),
869
- });
870
- return (
871
- <div className="grid gap-4 xl:grid-cols-2">
872
- <DataPanel title="Portability status" result={port.data}>
873
- {(data) => <PortabilityStatus data={data as Record<string, unknown>} />}
874
- </DataPanel>
875
- <Card>
876
- <CardHeader>
877
- <CardTitle className="flex items-center gap-2"><DatabaseBackup className="h-4 w-4" /> Export, backup, import</CardTitle>
878
- <CardDescription>Export, back up, or preview an import before changing your brain.</CardDescription>
879
- </CardHeader>
880
- <CardContent className="space-y-3">
881
- <div className="flex flex-wrap gap-2">
882
- <ActionButton label="Export Brain" action={() => latticeApi.graphExport()} />
883
- <ActionButton label="Create backup" action={() => latticeApi.graphBackup()} />
884
- </div>
885
- <Textarea value={artifact} onChange={(e) => setArtifact(e.target.value)} placeholder="Paste an exported Brain artifact to preview import" />
886
- <Button
887
- variant="outline"
888
- disabled={!artifact.trim() || importMutation.isPending}
889
- onClick={() => importMutation.mutate()}
890
- >
891
- Preview import
892
- </Button>
893
- {importMutation.data ? <OperationResult result={importMutation.data} successLabel="Import preview completed" /> : null}
894
- </CardContent>
895
- </Card>
896
- </div>
897
- );
898
- }
899
-
900
- function PortabilityStatus({ data }: { data: Record<string, unknown> }) {
901
- const stats = isRecord(data.stats) ? data.stats : {};
902
- const storage = isRecord(data.storage) ? data.storage : {};
903
- return (
904
- <div className="space-y-3">
905
- <StatGrid stats={[
906
- { label: "Brain format", value: data.graph_schema_version || data.schema_version || "reported" },
907
- { label: "Memories", value: (stats.total_nodes as number) || Object.values((stats.nodes as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
908
- { label: "Links", value: (stats.total_edges as number) || Object.values((stats.edges as Record<string, unknown>) || {}).reduce((sum: number, value) => sum + Number(value || 0), 0) },
909
- { label: "Storage", value: storage.engine || "reported" },
910
- ]} />
911
- <StructuredView value={data} />
912
- </div>
913
- );
914
- }