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.
- package/README.md +33 -72
- package/docs/CHANGELOG.md +52 -874
- package/docs/DEVELOPMENT.md +26 -3
- package/docs/LEGACY_COMPATIBILITY.md +3 -1
- package/docs/TRUST_MODEL.md +26 -26
- package/docs/WHY_LATTICE.md +28 -25
- package/docs/kg-schema.md +6 -4
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/embeddings.py +9 -1
- package/lattice_brain/graph/schema.py +6 -0
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/computer_use.py +2 -0
- package/latticeai/api/memory.py +13 -0
- package/latticeai/app_factory.py +43 -162
- package/latticeai/brain/__init__.py +1 -1
- package/latticeai/core/config.py +10 -0
- 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 +10 -0
- package/latticeai/core/workspace_graph_trace.py +132 -0
- package/latticeai/core/workspace_memory.py +75 -0
- package/latticeai/core/workspace_os.py +135 -1283
- package/latticeai/core/workspace_os_utils.py +132 -0
- package/latticeai/core/workspace_permissions.py +99 -0
- package/latticeai/core/workspace_plugins.py +97 -0
- package/latticeai/core/workspace_runs.py +612 -0
- package/latticeai/core/workspace_skills.py +114 -0
- package/latticeai/core/workspace_snapshots.py +195 -0
- package/latticeai/core/workspace_timeline.py +107 -0
- package/latticeai/runtime/audit_runtime.py +64 -0
- package/latticeai/runtime/config_runtime.py +4 -0
- package/latticeai/runtime/sso_runtime.py +52 -0
- package/latticeai/services/architecture_readiness.py +6 -5
- package/latticeai/services/memory_service.py +215 -7
- package/latticeai/services/model_capability_registry.py +68 -0
- package/latticeai/services/model_engines.py +603 -0
- package/latticeai/services/model_loading.py +482 -0
- package/latticeai/services/model_recommendation.py +5 -0
- package/latticeai/services/model_runtime.py +146 -837
- package/latticeai/services/product_readiness.py +19 -18
- 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-D9jIknFd.js +1 -0
- package/static/app/assets/Brain-CFOtWbPN.js +321 -0
- package/static/app/assets/{Capture-B9Tlhzqr.js → Capture-Q4WYzwr5.js} +1 -2
- package/static/app/assets/Library-C5Q2yWee.js +1 -0
- package/static/app/assets/System-BLbjdr1_.js +1 -0
- package/static/app/assets/core-CwxXejkd.js +1 -2
- package/static/app/assets/index-BqammyNu.js +16 -0
- package/static/app/assets/index-ty1iGgZu.css +2 -0
- package/static/app/assets/primitives-Br8uSfZ4.js +1 -0
- package/static/app/assets/{textarea-BZk6ybp5.js → textarea-BnhNs1_X.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/tools/__init__.py +2 -1
- package/tools/computer.py +21 -0
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +0 -61
- package/docs/HANDOVER_v3.6.0.md +0 -46
- package/docs/RUNTIME_HOOK_COVERAGE_v3.5.0.md +0 -56
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +0 -49
- package/docs/V2_ARCHITECTURE.md +0 -561
- package/docs/V3_2_AUDIT.md +0 -82
- package/docs/V3_BACKEND_ARCHITECTURE.md +0 -138
- package/docs/V3_FRONTEND.md +0 -140
- package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +0 -65
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +0 -70
- package/docs/V4_1_VALIDATION_REPORT.md +0 -47
- package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +0 -97
- package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +0 -91
- package/docs/V4_2_VALIDATION_REPORT.md +0 -89
- package/docs/V4_3_2_DEADCODE_AUDIT_REPORT.md +0 -174
- package/docs/V4_3_2_DOCUMENTATION_CLEANUP_REPORT.md +0 -81
- package/docs/V4_3_2_GITHUB_VERCEL_CHECK_REPORT.md +0 -75
- package/docs/V4_3_2_GRAPH_UX_REPORT.md +0 -48
- package/docs/V4_3_2_INDEPENDENT_AUDIT_PACKAGE.md +0 -209
- package/docs/V4_3_2_PRODUCT_POLISH_REPORT.md +0 -57
- package/docs/V4_3_2_SELF_AUDIT_REPORT.md +0 -63
- package/docs/V4_3_2_VALIDATION_REPORT.md +0 -97
- package/docs/V4_3_3_VALIDATION_REPORT.md +0 -46
- package/docs/V4_3_PORTABILITY_ARCHITECTURE.md +0 -69
- package/docs/V4_3_PRIVACY_AUDIT.md +0 -60
- package/docs/V4_3_PRODUCT_HARDENING_REPORT.md +0 -53
- package/docs/V4_3_VALIDATION_REPORT.md +0 -58
- package/docs/V4_4_0_EXTRACTION_REPORT.md +0 -239
- package/docs/V4_5_0_GEMMA_RUNTIME_COMPATIBILITY_REPORT.md +0 -49
- package/docs/V4_5_0_GRAPH_UX_REPORT.md +0 -34
- package/docs/V4_5_0_MODEL_RUNTIME_UX_REPORT.md +0 -40
- package/docs/V4_5_0_ONBOARDING_REPORT.md +0 -31
- package/docs/V4_5_0_PRODUCT_EXPERIENCE_RECOVERY_REPORT.md +0 -49
- package/docs/V4_5_0_VALIDATION_REPORT.md +0 -60
- package/docs/V4_5_1_GRAPH_EXPERIENCE_REPORT.md +0 -33
- package/docs/V4_5_1_MODEL_EXPERIENCE_REPORT.md +0 -37
- package/docs/V4_5_1_NAVIGATION_REPORT.md +0 -37
- package/docs/V4_5_1_ONBOARDING_REPORT.md +0 -29
- package/docs/V4_5_1_PRODUCT_REIMAGINING_REPORT.md +0 -61
- package/docs/V4_5_1_RC_ARTIFACTS.md +0 -44
- package/docs/V4_5_1_UX_REPORT.md +0 -45
- package/docs/V4_5_1_VALIDATION_REPORT.md +0 -55
- package/docs/V4_5_1_VISUAL_DESIGN_REPORT.md +0 -30
- package/docs/V4_6_0_LIVING_BRAIN_EXPERIENCE_REPORT.md +0 -72
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +0 -42
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +0 -42
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +0 -49
- package/docs/V4_7_2_INTUITIVE_BRAIN_UX_REPORT.md +0 -62
- package/docs/V4_BRAIN_ARCHITECTURE.md +0 -322
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +0 -555
- package/docs/V4_IMPLEMENTATION_PLAN.md +0 -470
- 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,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
|
-
}
|