pi-mega-compact 0.11.5 → 0.11.7

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 (147) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/cache-stripes.js +8 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +13 -0
  3. package/dist/extensions/dashboard-server/api-contracts/memory-map.js +1 -0
  4. package/dist/extensions/dashboard-server/routes-cache.js +28 -0
  5. package/dist/extensions/dashboard-server/routes-game.js +3 -0
  6. package/dist/extensions/dashboard-server/routes-memory-map.js +50 -0
  7. package/dist/extensions/dashboard-server/routes-setup.js +105 -0
  8. package/dist/extensions/dashboard-server/routes.js +3 -1
  9. package/dist/extensions/dashboard-server/server.js +7 -1
  10. package/dist/extensions/mega-compact.js +5 -0
  11. package/dist/extensions/mega-config.js +5 -0
  12. package/dist/extensions/mega-events/context-handler.js +10 -2
  13. package/dist/extensions/mega-events/perf-handler.js +83 -1
  14. package/dist/extensions/mega-events/separated-prompt.js +248 -0
  15. package/dist/extensions/mega-runtime/env-loader.js +56 -0
  16. package/dist/extensions/mega-runtime/reset-runtime.js +1 -0
  17. package/dist/extensions/mega-runtime/runtime-snapshot.js +1 -0
  18. package/dist/extensions/mega-runtime/runtime.js +1 -0
  19. package/dist/extensions/mega-runtime/snapshot.js +1 -0
  20. package/dist/extensions/mega-runtime/widget.js +5 -0
  21. package/dist/src/cache-stripe-impl.js +288 -0
  22. package/dist/src/cache-stripe.js +11 -0
  23. package/dist/src/config/stopwords.js +24 -0
  24. package/dist/src/config.js +17 -0
  25. package/dist/src/memoryGraph.js +299 -0
  26. package/dist/src/monitoring.js +13 -0
  27. package/dist/src/queryExpansion.js +163 -0
  28. package/dist/src/queryReformulation/cache.js +123 -0
  29. package/dist/src/queryReformulation/rrf.js +29 -0
  30. package/dist/src/queryReformulation/tfidf.js +61 -0
  31. package/dist/src/queryReformulation/vagueness.js +33 -0
  32. package/dist/src/queryReformulation.js +225 -0
  33. package/dist/src/recall.js +136 -1
  34. package/dist/src/recallMetrics.js +258 -0
  35. package/dist/src/store/sqlite/cache-stripes.js +133 -0
  36. package/dist/src/store/sqlite/fts5-search.js +68 -0
  37. package/dist/src/store/sqlite/perf-samples.js +1 -0
  38. package/dist/src/store/sqlite/schema.js +60 -1
  39. package/dist/src/store/sqlite.js +1 -0
  40. package/dist/src/store/vectorIndex.js +29 -5
  41. package/dist/src/tiered-router/types.js +18 -0
  42. package/dist/src/tieredRouter.js +421 -0
  43. package/dist/src/vectorStore.js +8 -6
  44. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js → AchievementTiles-DR9cLGDL.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BoL7qBOJ.js.map → AchievementTiles-DR9cLGDL.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js → AchievementsTab-svNUgJ1e.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{AchievementsTab-BFDE3HYc.js.map → AchievementsTab-svNUgJ1e.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/CacheTab-ASwhOMjR.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js → ConfigTab-CmmWheuW.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{ConfigTab-DDFE-ckA.js.map → ConfigTab-CmmWheuW.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js → EventsTab-CFE7jLsa.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{EventsTab-B6UxZ7PM.js.map → EventsTab-CFE7jLsa.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/GameTab-CJAX_th_.js +2 -0
  55. package/extensions/dashboard-client/dist/assets/{GameTab-DehUX-KE.js.map → GameTab-CJAX_th_.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js → MaintenanceTab-P4SUsvMf.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{MaintenanceTab-0GWRt4wn.js.map → MaintenanceTab-P4SUsvMf.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js +2 -0
  59. package/extensions/dashboard-client/dist/assets/MemoryMapTab-BBiH76eF.js.map +1 -0
  60. package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js → MetricsTab-CHQscixy.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{MetricsTab-C3rwMVsN.js.map → MetricsTab-CHQscixy.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js → OverviewTab-DBs4DJdd.js} +2 -2
  63. package/extensions/dashboard-client/dist/assets/{OverviewTab-39O9_x5j.js.map → OverviewTab-DBs4DJdd.js.map} +1 -1
  64. package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js → ReposTab-DeFz76L5.js} +2 -2
  65. package/extensions/dashboard-client/dist/assets/{ReposTab-Cfj6Q5sH.js.map → ReposTab-DeFz76L5.js.map} +1 -1
  66. package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js → SessionsTab-BWNsj1OB.js} +2 -2
  67. package/extensions/dashboard-client/dist/assets/{SessionsTab-DXDTCi3r.js.map → SessionsTab-BWNsj1OB.js.map} +1 -1
  68. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js +2 -0
  69. package/extensions/dashboard-client/dist/assets/SetupTab-uS6A0xmw.js.map +1 -0
  70. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js → TimeSavedCard-CiqCOQuL.js} +2 -2
  71. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-Cw1dJ4e9.js.map → TimeSavedCard-CiqCOQuL.js.map} +1 -1
  72. package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js → TopicsTab-BWWQxltv.js} +2 -2
  73. package/extensions/dashboard-client/dist/assets/{TopicsTab-BAw972BV.js.map → TopicsTab-BWWQxltv.js.map} +1 -1
  74. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js → TurnsTab-CVz4f4-o.js} +2 -2
  75. package/extensions/dashboard-client/dist/assets/{TurnsTab-Cf5mGIdl.js.map → TurnsTab-CVz4f4-o.js.map} +1 -1
  76. package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +1 -0
  77. package/extensions/dashboard-client/dist/assets/{index-4gQ7auad.js → index-DrktNOoT.js} +11 -11
  78. package/extensions/dashboard-client/dist/assets/index-DrktNOoT.js.map +1 -0
  79. package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js → useSSE-Bf5LOLSi.js} +2 -2
  80. package/extensions/dashboard-client/dist/assets/{useSSE-DNgMuyP6.js.map → useSSE-Bf5LOLSi.js.map} +1 -1
  81. package/extensions/dashboard-client/dist/index.html +2 -2
  82. package/extensions/dashboard-client/src/App.tsx +24 -8
  83. package/extensions/dashboard-client/src/api/client.ts +15 -0
  84. package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +100 -0
  85. package/extensions/dashboard-client/src/components/StripeDistributionCard.tsx +110 -0
  86. package/extensions/dashboard-client/src/components/TabBar.tsx +49 -4
  87. package/extensions/dashboard-client/src/main.tsx +1 -0
  88. package/extensions/dashboard-client/src/styles/base.css +30 -0
  89. package/extensions/dashboard-client/src/styles/cache.css +167 -0
  90. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +75 -13
  91. package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +440 -0
  92. package/extensions/dashboard-client/src/tabs/SetupTab.tsx +65 -2
  93. package/extensions/dashboard-server/api-contracts/cache-stripes.ts +49 -0
  94. package/extensions/dashboard-server/api-contracts/endpoints.ts +31 -1
  95. package/extensions/dashboard-server/api-contracts/index.ts +16 -1
  96. package/extensions/dashboard-server/api-contracts/memory-map.ts +50 -0
  97. package/extensions/dashboard-server/api-contracts/setup.ts +22 -0
  98. package/extensions/dashboard-server/routes-cache.ts +36 -1
  99. package/extensions/dashboard-server/routes-game.ts +3 -0
  100. package/extensions/dashboard-server/routes-memory-map.ts +105 -0
  101. package/extensions/dashboard-server/routes-setup.ts +112 -0
  102. package/extensions/dashboard-server/routes.ts +3 -1
  103. package/extensions/dashboard-server/server.ts +6 -0
  104. package/extensions/mega-compact.ts +5 -0
  105. package/extensions/mega-config.ts +17 -0
  106. package/extensions/mega-events/context-handler.ts +10 -2
  107. package/extensions/mega-events/perf-handler.ts +111 -1
  108. package/extensions/mega-events/separated-prompt.ts +329 -0
  109. package/extensions/mega-runtime/env-loader.ts +55 -0
  110. package/extensions/mega-runtime/helpers.ts +2 -0
  111. package/extensions/mega-runtime/reset-runtime.ts +1 -0
  112. package/extensions/mega-runtime/runtime-snapshot.ts +1 -0
  113. package/extensions/mega-runtime/runtime.ts +1 -0
  114. package/extensions/mega-runtime/snapshot.ts +6 -0
  115. package/extensions/mega-runtime/widget-types.ts +3 -0
  116. package/extensions/mega-runtime/widget.ts +5 -0
  117. package/package.json +1 -1
  118. package/src/cache-stripe-impl.ts +385 -0
  119. package/src/cache-stripe.ts +18 -0
  120. package/src/config/stopwords.ts +25 -0
  121. package/src/config.ts +26 -0
  122. package/src/memoryGraph.ts +438 -0
  123. package/src/monitoring.ts +35 -0
  124. package/src/queryExpansion.ts +206 -0
  125. package/src/queryReformulation/cache.ts +169 -0
  126. package/src/queryReformulation/rrf.ts +51 -0
  127. package/src/queryReformulation/tfidf.ts +79 -0
  128. package/src/queryReformulation/vagueness.ts +38 -0
  129. package/src/queryReformulation.ts +355 -0
  130. package/src/recall.ts +168 -4
  131. package/src/recallMetrics.ts +388 -0
  132. package/src/store/sqlite/cache-stripes.ts +196 -0
  133. package/src/store/sqlite/fts5-search.ts +90 -0
  134. package/src/store/sqlite/perf-samples.ts +3 -1
  135. package/src/store/sqlite/schema.ts +60 -1
  136. package/src/store/sqlite.ts +1 -0
  137. package/src/store/vectorIndex.ts +38 -7
  138. package/src/tiered-router/types.ts +64 -0
  139. package/src/tieredRouter.ts +489 -0
  140. package/src/vectorStore.ts +9 -5
  141. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js +0 -2
  142. package/extensions/dashboard-client/dist/assets/CacheTab-eKVc-NV1.js.map +0 -1
  143. package/extensions/dashboard-client/dist/assets/GameTab-DehUX-KE.js +0 -2
  144. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js +0 -2
  145. package/extensions/dashboard-client/dist/assets/SetupTab-mMlfunJa.js.map +0 -1
  146. package/extensions/dashboard-client/dist/assets/index-4gQ7auad.js.map +0 -1
  147. package/extensions/dashboard-client/dist/assets/index-BEdKlpAB.css +0 -1
@@ -1,22 +1,36 @@
1
1
  /**
2
- * dashboard-client/src/tabs/CacheTab.tsx Cache tab (B.3).
2
+ * dashboard-client/src/tabs/CacheTab.tsx -- Cache tab (B.3 + A3).
3
3
  *
4
- * Two sections, each with its own header:
5
- * 1. Provider Prompt Cache ProviderCacheCard (/api/provider-cache)
6
- * 2. Mega-Compact Dedup Cache CacheHitsCard + TimeSavedCard (/api/snapshot)
4
+ * Sections:
5
+ * 1. Provider Prompt Cache -> ProviderCacheCard (/api/provider-cache)
6
+ * 2. Cache Stripe Distribution -> StripeDistributionCard (/api/cache-stripes)
7
+ * 3. Cache Hit-Rate Trend -> CacheHitRateTrendCard (/api/perf)
8
+ * 4. Mega-Compact Dedup Cache -> CacheHitsCard + TimeSavedCard (/api/snapshot)
7
9
  *
8
- * Both fetch at 5s polling (same interval). Loading / error shown per-section
9
- * only when its own fetch fails; the other section keeps rendering.
10
+ * Each section fetches independently; failures in one do not affect others.
11
+ * 5s polling on provider cache and activity snapshot; 15s on stripes + trend.
10
12
  */
11
13
 
12
14
  import type React from "react";
13
15
  import { useCallback } from "react";
14
- import type { SnapshotResponse, ProviderCacheResponse } from "@contracts";
16
+ import type {
17
+ SnapshotResponse,
18
+ ProviderCacheResponse,
19
+ CacheStripesResponse,
20
+ PerfResponse,
21
+ } from "@contracts";
15
22
  import { useApi } from "../hooks/useApi";
16
- import { fetchSnapshot, fetchProviderCache } from "../api/client";
23
+ import {
24
+ fetchSnapshot,
25
+ fetchProviderCache,
26
+ fetchCacheStripes,
27
+ fetchPerf,
28
+ } from "../api/client";
17
29
  import { CacheHitsCard } from "../components/CacheHitsCard";
18
30
  import { TimeSavedCard } from "../components/TimeSavedCard";
19
31
  import { ProviderCacheCard } from "../components/ProviderCacheCard";
32
+ import { StripeDistributionCard } from "../components/StripeDistributionCard";
33
+ import { CacheHitRateTrendCard } from "../components/CacheHitRateTrendCard";
20
34
 
21
35
  export default function CacheTab(): React.ReactElement {
22
36
  /* --- Provider prompt cache (5s poll) --- */
@@ -29,6 +43,26 @@ export default function CacheTab(): React.ReactElement {
29
43
  { pollInterval: 5000 },
30
44
  );
31
45
 
46
+ /* --- Cache stripe distribution (15s poll) --- */
47
+ const {
48
+ data: stripes,
49
+ loading: stripesLoading,
50
+ error: stripesError,
51
+ } = useApi<CacheStripesResponse>(
52
+ useCallback(() => fetchCacheStripes(), []),
53
+ { pollInterval: 15000 },
54
+ );
55
+
56
+ /* --- Cache hit-rate trend from perf samples (15s poll) --- */
57
+ const {
58
+ data: perf,
59
+ loading: perfLoading,
60
+ error: perfError,
61
+ } = useApi<PerfResponse>(
62
+ useCallback(() => fetchPerf({ minutes: 30 }), []),
63
+ { pollInterval: 15000 },
64
+ );
65
+
32
66
  /* --- Dedup cache snapshot (5s poll) --- */
33
67
  const {
34
68
  data: snapshot,
@@ -42,11 +76,11 @@ export default function CacheTab(): React.ReactElement {
42
76
  return (
43
77
  <div className="cache-tab">
44
78
  {/* ==============================================================
45
- Section 1 Provider Prompt Cache
79
+ Section 1 -- Provider Prompt Cache
46
80
  ============================================================== */}
47
81
  <h2>Provider Prompt Cache</h2>
48
82
  {pcLoading ? (
49
- <p className="tab-stub">Loading…</p>
83
+ <p className="tab-stub">Loading...</p>
50
84
  ) : pcError ? (
51
85
  <p className="tab-stub">
52
86
  Provider cache unavailable: {pcError.message}
@@ -64,16 +98,44 @@ export default function CacheTab(): React.ReactElement {
64
98
  modelLabel={providerCache.savings?.model ?? null}
65
99
  firstTurnAt={providerCache.cache.firstTurnAt}
66
100
  latestTurnAt={providerCache.cache.latestTurnAt}
67
- byModel={providerCache.cache.byModel ?? []}
101
+ byModel={providerCache.cache.byModel ?? []}
68
102
  />
69
103
  ) : null}
70
104
 
71
105
  {/* ==============================================================
72
- Section 2 Mega-Compact Dedup Cache
106
+ Section 2 -- Cache Stripe Distribution
107
+ ============================================================== */}
108
+ <h2>Cache Stripe Distribution</h2>
109
+ {stripesLoading ? (
110
+ <p className="tab-stub">Loading...</p>
111
+ ) : stripesError ? (
112
+ <p className="tab-stub">
113
+ Cache stripe data unavailable: {stripesError.message}
114
+ </p>
115
+ ) : stripes ? (
116
+ <StripeDistributionCard data={stripes} />
117
+ ) : null}
118
+
119
+ {/* ==============================================================
120
+ Section 3 -- Cache Hit-Rate Trend
121
+ ============================================================== */}
122
+ <h2>Cache Hit-Rate Trend</h2>
123
+ {perfLoading ? (
124
+ <p className="tab-stub">Loading...</p>
125
+ ) : perfError ? (
126
+ <p className="tab-stub">
127
+ Hit-rate trend unavailable: {perfError.message}
128
+ </p>
129
+ ) : perf ? (
130
+ <CacheHitRateTrendCard perf={perf} />
131
+ ) : null}
132
+
133
+ {/* ==============================================================
134
+ Section 4 -- Mega-Compact Dedup Cache
73
135
  ============================================================== */}
74
136
  <h2>Mega-Compact Dedup Cache</h2>
75
137
  {snapLoading ? (
76
- <p className="tab-stub">Loading…</p>
138
+ <p className="tab-stub">Loading...</p>
77
139
  ) : snapError ? (
78
140
  <p className="tab-stub">Snapshot unavailable: {snapError.message}</p>
79
141
  ) : (
@@ -0,0 +1,440 @@
1
+ /**
2
+ * MemoryMapTab.tsx — S46 visual memory map (force-directed graph of checkpoints).
3
+ *
4
+ * Fetches graph data from /api/memory-map and renders an SVG-based
5
+ * force-directed layout. Uses no external graph library (no @xyflow/react).
6
+ * The simulation uses Coulomb repulsion + Hooke attraction + center gravity.
7
+ */
8
+ import type React from "react";
9
+ import { useEffect, useRef, useState, useCallback, useMemo } from "react";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types (mirror of api-contracts/memory-map.ts — client-side only)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ interface GraphNode {
16
+ id: string;
17
+ sessionId: string;
18
+ label: string;
19
+ summaryTruncated: string;
20
+ tokenEstimate: number;
21
+ timestamp: number;
22
+ dedupStatus: string | undefined;
23
+ raptorLevel: number;
24
+ topicSummary: string | undefined;
25
+ decisionCount: number;
26
+ textSnippet: string;
27
+ }
28
+
29
+ interface GraphEdge {
30
+ source: string;
31
+ target: string;
32
+ weight: number;
33
+ type: "temporal" | "semantic" | "raptor_parent";
34
+ }
35
+
36
+ interface GraphMetadata {
37
+ sessionCount: number;
38
+ totalCheckpoints: number;
39
+ totalEdges: number;
40
+ semanticEdgeCount: number;
41
+ temporalEdgeCount: number;
42
+ raptorEdgeCount: number;
43
+ similarityThresholdUsed: number;
44
+ builtAt: number;
45
+ }
46
+
47
+ interface MemoryMapResponse {
48
+ nodes: GraphNode[];
49
+ edges: GraphEdge[];
50
+ metadata: GraphMetadata;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Force-directed layout types
55
+ // ---------------------------------------------------------------------------
56
+
57
+ interface Position {
58
+ x: number;
59
+ y: number;
60
+ }
61
+
62
+ interface LayoutNode extends GraphNode {
63
+ pos: Position;
64
+ vx: number;
65
+ vy: number;
66
+ pinned: boolean;
67
+ }
68
+
69
+ interface LayoutEdge {
70
+ source: number;
71
+ target: number;
72
+ weight: number;
73
+ type: GraphEdge["type"];
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Force simulation constants
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const REPULSION = 5_000;
81
+ const ATTRACTION = 0.005;
82
+ const CENTER_GRAVITY = 0.01;
83
+ const DAMPING = 0.9;
84
+ const MIN_VELOCITY = 0.1;
85
+ const MAX_ITERATIONS = 300;
86
+ const EDGE_ALPHA = 0.3;
87
+ const SVG_WIDTH = 900;
88
+ const SVG_HEIGHT = 600;
89
+ const PADDING = 40;
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Helpers
93
+ // ---------------------------------------------------------------------------
94
+
95
+ function edgeColor(type: GraphEdge["type"]): string {
96
+ switch (type) {
97
+ case "temporal":
98
+ return "#6366f1"; // indigo
99
+ case "semantic":
100
+ return "#22c55e"; // green
101
+ case "raptor_parent":
102
+ return "#f59e0b"; // amber
103
+ }
104
+ }
105
+
106
+ function nodeOpacity(dedupStatus: string | undefined): number {
107
+ if (!dedupStatus || dedupStatus === "active") return 1;
108
+ return 0.5;
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Force simulation engine
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function buildLayout(data: MemoryMapResponse): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
116
+ const nodes: LayoutNode[] = data.nodes.map((n, i) => {
117
+ const angle = (2 * Math.PI * i) / Math.max(data.nodes.length, 1);
118
+ const radius = 200;
119
+ return {
120
+ ...n,
121
+ pos: { x: Math.cos(angle) * radius + 300, y: Math.sin(angle) * radius + 200 },
122
+ vx: 0,
123
+ vy: 0,
124
+ pinned: false,
125
+ };
126
+ });
127
+
128
+ const nodeIndex = new Map<string, number>();
129
+ nodes.forEach((n, i) => nodeIndex.set(n.id, i));
130
+
131
+ const edges: LayoutEdge[] = data.edges
132
+ .map((e) => {
133
+ const si = nodeIndex.get(e.source);
134
+ const ti = nodeIndex.get(e.target);
135
+ if (si === undefined || ti === undefined || si === ti) return null;
136
+ return { source: si, target: ti, weight: e.weight, type: e.type };
137
+ })
138
+ .filter((e): e is LayoutEdge => e !== null);
139
+
140
+ return { nodes, edges };
141
+ }
142
+
143
+ function applyForces(layout: { nodes: LayoutNode[]; edges: LayoutEdge[] }): void {
144
+ const { nodes, edges } = layout;
145
+ const n = nodes.length;
146
+ if (n === 0) return;
147
+
148
+ for (let i = 0; i < n; i++) {
149
+ if (nodes[i].pinned) continue;
150
+ let fx = 0;
151
+ let fy = 0;
152
+
153
+ for (let j = 0; j < n; j++) {
154
+ if (i === j) continue;
155
+ const dx = nodes[i].pos.x - nodes[j].pos.x;
156
+ const dy = nodes[i].pos.y - nodes[j].pos.y;
157
+ const distSq = dx * dx + dy * dy || 1;
158
+ const force = REPULSION / distSq;
159
+ const dist = Math.sqrt(distSq);
160
+ fx += (dx / dist) * force;
161
+ fy += (dy / dist) * force;
162
+ }
163
+
164
+ for (const e of edges) {
165
+ let other: number;
166
+ if (e.source === i) other = e.target;
167
+ else if (e.target === i) other = e.source;
168
+ else continue;
169
+ const dx = nodes[other].pos.x - nodes[i].pos.x;
170
+ const dy = nodes[other].pos.y - nodes[i].pos.y;
171
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
172
+ fx += dx * ATTRACTION * e.weight;
173
+ fy += dy * ATTRACTION * e.weight;
174
+ }
175
+
176
+ fx -= nodes[i].pos.x * CENTER_GRAVITY;
177
+ fy -= nodes[i].pos.y * CENTER_GRAVITY;
178
+
179
+ nodes[i].vx = (nodes[i].vx + fx) * DAMPING;
180
+ nodes[i].vy = (nodes[i].vy + fy) * DAMPING;
181
+
182
+ const speed = Math.sqrt(nodes[i].vx * nodes[i].vx + nodes[i].vy * nodes[i].vy);
183
+ if (speed > 50) {
184
+ nodes[i].vx = (nodes[i].vx / speed) * 50;
185
+ nodes[i].vy = (nodes[i].vy / speed) * 50;
186
+ }
187
+
188
+ if (speed > MIN_VELOCITY) {
189
+ nodes[i].pos.x += nodes[i].vx;
190
+ nodes[i].pos.y += nodes[i].vy;
191
+ }
192
+ }
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Component
197
+ // ---------------------------------------------------------------------------
198
+
199
+ const MemoryMapTab: React.FC = () => {
200
+ const [data, setData] = useState<MemoryMapResponse | null>(null);
201
+ const [loading, setLoading] = useState<boolean>(true);
202
+ const [error, setError] = useState<string | null>(null);
203
+ const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
204
+ const [searchQuery, setSearchQuery] = useState<string>("");
205
+ const [filterSession, setFilterSession] = useState<string>("");
206
+ const [frame, setFrame] = useState<number>(0);
207
+
208
+ const layoutRef = useRef<{ nodes: LayoutNode[]; edges: LayoutEdge[] } | null>(null);
209
+ const animRef = useRef<number>(0);
210
+
211
+ // Fetch graph data when filterSession changes
212
+ useEffect(() => {
213
+ let cancelled = false;
214
+ const params = new URLSearchParams();
215
+ if (filterSession) params.set("sessionId", filterSession);
216
+ params.set("threshold", "0.7");
217
+ params.set("maxEdgesPerNode", "4");
218
+
219
+ setLoading(true);
220
+ setError(null);
221
+
222
+ fetch(`/api/memory-map?${params.toString()}`)
223
+ .then((r) => {
224
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
225
+ return r.json() as Promise<MemoryMapResponse>;
226
+ })
227
+ .then((d) => {
228
+ if (!cancelled) {
229
+ setData(d);
230
+ layoutRef.current = buildLayout(d);
231
+ setFrame(1);
232
+ setLoading(false);
233
+ }
234
+ })
235
+ .catch((e: unknown) => {
236
+ if (!cancelled) {
237
+ setError(e instanceof Error ? e.message : "Unknown error");
238
+ setLoading(false);
239
+ }
240
+ });
241
+
242
+ return () => {
243
+ cancelled = true;
244
+ cancelAnimationFrame(animRef.current);
245
+ };
246
+ }, [filterSession]);
247
+
248
+ // Run force simulation via requestAnimationFrame
249
+ useEffect(() => {
250
+ if (!layoutRef.current || frame === 0) return;
251
+
252
+ const layout = layoutRef.current;
253
+ let iter = 0;
254
+
255
+ const tick = () => {
256
+ if (iter >= MAX_ITERATIONS) return;
257
+ applyForces(layout);
258
+ iter++;
259
+ setFrame(iter);
260
+ animRef.current = requestAnimationFrame(tick);
261
+ };
262
+
263
+ animRef.current = requestAnimationFrame(tick);
264
+ return () => cancelAnimationFrame(animRef.current);
265
+ }, [frame]);
266
+
267
+ // Toggle pin / select a node
268
+ const handleNodeClick = useCallback((node: LayoutNode, index: number) => {
269
+ const layout = layoutRef.current;
270
+ if (!layout) return;
271
+ layout.nodes[index].pinned = !layout.nodes[index].pinned;
272
+ if (!layout.nodes[index].pinned) {
273
+ layout.nodes[index].vx = 0;
274
+ layout.nodes[index].vy = 0;
275
+ }
276
+ setSelectedNode((prev) => (prev?.id === node.id ? null : (node as unknown as GraphNode)));
277
+ setFrame((s) => s + 1);
278
+ }, []);
279
+
280
+ // Filtered node indices for search
281
+ const filteredSet = useMemo<Set<number> | null>(() => {
282
+ const layout = layoutRef.current;
283
+ if (!layout || !searchQuery) return null;
284
+ const q = searchQuery.toLowerCase();
285
+ return new Set(
286
+ layout.nodes
287
+ .map((n, i) =>
288
+ n.summaryTruncated.toLowerCase().includes(q) || n.label.toLowerCase().includes(q) ? i : -1,
289
+ )
290
+ .filter((i) => i >= 0),
291
+ );
292
+ }, [searchQuery]);
293
+
294
+ // ---------- Loading state ----------
295
+
296
+ if (loading) {
297
+ return <div className="memory-map-loading">Loading memory graph...</div>;
298
+ }
299
+
300
+ // ---------- Error state ----------
301
+
302
+ if (error) {
303
+ return (
304
+ <div className="memory-map-error">
305
+ <p>Failed to load memory map: {error}</p>
306
+ <button
307
+ className="memory-map-retry-btn"
308
+ onClick={() => setFrame((s) => s + 1)}
309
+ >
310
+ Retry
311
+ </button>
312
+ </div>
313
+ );
314
+ }
315
+
316
+ // ---------- Empty state ----------
317
+
318
+ if (!data || !layoutRef.current) {
319
+ return <div className="memory-map-empty">No memory data available.</div>;
320
+ }
321
+
322
+ const layout = layoutRef.current;
323
+
324
+ // ---------- Render ----------
325
+
326
+ return (
327
+ <div className="memory-map-container">
328
+ {/* Toolbar */}
329
+ <div className="memory-map-toolbar">
330
+ <input
331
+ type="text"
332
+ placeholder="Search memories..."
333
+ value={searchQuery}
334
+ onInput={(e: React.FormEvent<HTMLInputElement>) =>
335
+ setSearchQuery((e.target as HTMLInputElement).value)
336
+ }
337
+ className="memory-map-search"
338
+ />
339
+ <span className="memory-map-stats">
340
+ {data.nodes.length} memories, {data.edges.length} edges
341
+ <br />
342
+ Sessions: {data.metadata.sessionCount} | Threshold:{" "}
343
+ {data.metadata.similarityThresholdUsed.toFixed(2)}
344
+ </span>
345
+ </div>
346
+
347
+ {/* Legend */}
348
+ <div className="memory-map-legend">
349
+ <span style={{ color: edgeColor("semantic") }}>Semantic</span>
350
+ <span style={{ color: edgeColor("temporal") }}>Temporal</span>
351
+ <span style={{ color: edgeColor("raptor_parent") }}>Raptor</span>
352
+ </div>
353
+
354
+ {/* SVG graph */}
355
+ <svg
356
+ width={SVG_WIDTH}
357
+ height={SVG_HEIGHT}
358
+ viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
359
+ className="memory-map-svg"
360
+ >
361
+ {/* Edges */}
362
+ {layout.edges.map((e, i) => {
363
+ const src = layout.nodes[e.source];
364
+ const tgt = layout.nodes[e.target];
365
+ const color = edgeColor(e.type);
366
+ const strokeW = Math.max(1, e.weight * 2.5);
367
+ return (
368
+ <line
369
+ key={`edge-${i}`}
370
+ x1={PADDING + (src.pos.x / (layout.nodes.length || 1)) * (SVG_WIDTH - 2 * PADDING)}
371
+ y1={PADDING + (src.pos.y / (layout.nodes.length || 1)) * (SVG_HEIGHT - 2 * PADDING)}
372
+ x2={PADDING + (tgt.pos.x / (layout.nodes.length || 1)) * (SVG_WIDTH - 2 * PADDING)}
373
+ y2={PADDING + (tgt.pos.y / (layout.nodes.length || 1)) * (SVG_HEIGHT - 2 * PADDING)}
374
+ stroke={color}
375
+ strokeWidth={strokeW}
376
+ opacity={EDGE_ALPHA}
377
+ />
378
+ );
379
+ })}
380
+
381
+ {/* Nodes */}
382
+ {layout.nodes.map((n, i) => {
383
+ const cx =
384
+ PADDING + (n.pos.x / (layout.nodes.length || 1)) * (SVG_WIDTH - 2 * PADDING);
385
+ const cy =
386
+ PADDING + (n.pos.y / (layout.nodes.length || 1)) * (SVG_HEIGHT - 2 * PADDING);
387
+ const radius = Math.min(20, 8 + n.tokenEstimate / 500);
388
+ const isFiltered = filteredSet ? filteredSet.has(i) : true;
389
+ const isSelected = selectedNode?.id === n.id;
390
+ const nodeLabel = n.label.length > 20 ? n.label.slice(0, 18) + "..." : n.label;
391
+
392
+ return (
393
+ <g key={`node-${i}`} className="memory-map-node-group">
394
+ <circle
395
+ cx={cx}
396
+ cy={cy}
397
+ r={isSelected ? radius + 3 : radius}
398
+ fill={isFiltered ? (n.raptorLevel > 0 ? "#f59e0b" : "#6366f1") : "#d1d5db"}
399
+ opacity={nodeOpacity(n.dedupStatus)}
400
+ stroke={isSelected ? "#ef4444" : "none"}
401
+ strokeWidth={isSelected ? 2 : 0}
402
+ className="memory-map-node"
403
+ onClick={() => handleNodeClick(n, i)}
404
+ />
405
+ <text
406
+ x={cx}
407
+ y={cy + radius + 12}
408
+ textAnchor="middle"
409
+ fontSize="9px"
410
+ fill="#374151"
411
+ className="memory-map-label"
412
+ >
413
+ {nodeLabel}
414
+ </text>
415
+ </g>
416
+ );
417
+ })}
418
+ </svg>
419
+
420
+ {/* Node detail panel */}
421
+ {selectedNode ? (
422
+ <div className="memory-map-detail">
423
+ <h3>{selectedNode.label}</h3>
424
+ <p>{selectedNode.summaryTruncated}</p>
425
+ {selectedNode.topicSummary ? (
426
+ <p className="memory-map-topic">Topic: {selectedNode.topicSummary}</p>
427
+ ) : null}
428
+ <p>
429
+ Tokens: {selectedNode.tokenEstimate} | Decisions: {selectedNode.decisionCount}
430
+ </p>
431
+ <p>Session: {selectedNode.sessionId}</p>
432
+ <p>Raptor Level: {selectedNode.raptorLevel}</p>
433
+ <p className="memory-map-snippet">{selectedNode.textSnippet}</p>
434
+ </div>
435
+ ) : null}
436
+ </div>
437
+ );
438
+ };
439
+
440
+ export default MemoryMapTab;
@@ -10,8 +10,8 @@
10
10
 
11
11
  import type React from "react";
12
12
  import { useState, useEffect, useCallback } from "react";
13
- import type { SetupStatusResponse, SetupDetectResponse } from "@contracts";
14
- import { fetchSetupStatus, fetchSetupDetect } from "../api/client";
13
+ import type { SetupStatusResponse, SetupDetectResponse, SetupConfigureResponse } from "@contracts";
14
+ import { fetchSetupStatus, fetchSetupDetect, configureEmbedder } from "../api/client";
15
15
 
16
16
  // ---------------------------------------------------------------------------
17
17
  // Style constants
@@ -90,6 +90,25 @@ export default function SetupTab(): React.ReactElement {
90
90
  const [statusError, setStatusError] = useState<string | null>(null);
91
91
  const [detectError, setDetectError] = useState<string | null>(null);
92
92
  const [runningDetect, setRunningDetect] = useState(false);
93
+ const [configuring, setConfiguring] = useState<string | null>(null);
94
+ const [configResult, setConfigResult] = useState<SetupConfigureResponse | null>(null);
95
+ const [configError, setConfigError] = useState<string | null>(null);
96
+
97
+ const applyEmbedder = useCallback((embedder: "ollama" | "llama" | "trigram") => {
98
+ setConfiguring(embedder);
99
+ setConfigError(null);
100
+ setConfigResult(null);
101
+ configureEmbedder({ embedder })
102
+ .then((r) => {
103
+ setConfigResult(r);
104
+ setConfiguring(null);
105
+ loadStatus();
106
+ })
107
+ .catch((e: unknown) => {
108
+ setConfigError(e instanceof Error ? e.message : String(e));
109
+ setConfiguring(null);
110
+ });
111
+ }, [loadStatus]);
93
112
 
94
113
  const loadStatus = useCallback(() => {
95
114
  fetchSetupStatus()
@@ -319,6 +338,50 @@ export default function SetupTab(): React.ReactElement {
319
338
  Or run <code>/megasetup</code> in the chat for an interactive wizard.
320
339
  </span>
321
340
  </div>
341
+
342
+ {/* Configure buttons — appear after detection runs */}
343
+ {detect && (
344
+ <div style={{ marginTop: "1rem", paddingTop: "0.75rem", borderTop: "1px solid #2a2a4e" }}>
345
+ <div style={{ fontSize: "0.9rem", fontWeight: 600, marginBottom: "0.5rem" }}>
346
+ Configure
347
+ </div>
348
+ <div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
349
+ <button
350
+ style={{ ...styles.button, opacity: detect.ollama?.installed ? 1 : 0.4 }}
351
+ onClick={() => applyEmbedder("ollama")}
352
+ disabled={!detect.ollama?.installed || configuring !== null}
353
+ >
354
+ {configuring === "ollama" ? "Writing..." : "Use Ollama"}
355
+ </button>
356
+ <button
357
+ style={{ ...styles.button, opacity: detect.llamaCpp?.installed ? 1 : 0.4 }}
358
+ onClick={() => applyEmbedder("llama")}
359
+ disabled={!detect.llamaCpp?.installed || configuring !== null}
360
+ >
361
+ {configuring === "llama" ? "Writing..." : "Use llama.cpp"}
362
+ </button>
363
+ <button
364
+ style={{ ...styles.button, background: "#444" }}
365
+ onClick={() => applyEmbedder("trigram")}
366
+ disabled={configuring !== null}
367
+ >
368
+ {configuring === "trigram" ? "Writing..." : "Use Trigram (default)"}
369
+ </button>
370
+ </div>
371
+ {configResult && (
372
+ <div style={styles.warning}>
373
+ {configResult.alreadyActive
374
+ ? "Already active — no restart needed."
375
+ : `Configured ${configResult.embedder}. Restart pi to activate (config written to ${configResult.envPath}).`}
376
+ </div>
377
+ )}
378
+ {configError && (
379
+ <p style={{ color: "#f44336", fontSize: "0.85rem", marginTop: "0.5rem" }}>
380
+ Configure error: {configError}
381
+ </p>
382
+ )}
383
+ </div>
384
+ )}
322
385
  </div>
323
386
 
324
387
  {/* Help Section */}