pi-mega-compact 0.13.4 → 0.13.6
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 +11 -8
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +50 -0
- package/dist/extensions/dashboard-server/api-contracts/rag-metrics.js +1 -0
- package/dist/extensions/dashboard-server/api-contracts/rag-settings.js +3 -3
- package/dist/extensions/dashboard-server/api-contracts/wiki.js +1 -0
- package/dist/extensions/dashboard-server/routes-rag-metrics.js +60 -0
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +167 -0
- package/dist/extensions/dashboard-server/routes-rag-settings.js +136 -77
- package/dist/extensions/dashboard-server/routes-wiki-helpers.js +97 -0
- package/dist/extensions/dashboard-server/routes-wiki.js +330 -0
- package/dist/extensions/dashboard-server/routes.js +2 -0
- package/dist/extensions/dashboard-server/server.js +17 -3
- package/dist/extensions/mega-events/context-handler/afterCompact.js +35 -2
- package/dist/extensions/mega-pipeline/recall.js +31 -0
- package/dist/extensions/mega-turn-store.js +2 -1
- package/dist/src/config/turns.js +10 -0
- package/dist/src/config.js +2 -0
- package/dist/src/recall/async.js +10 -1
- package/dist/src/recall/format.js +10 -1
- package/dist/src/recall/hydeTelemetry.js +22 -0
- package/dist/src/recall/sync.js +28 -2
- package/dist/src/store/turns/hydeStore.js +32 -0
- package/dist/src/store/turns/index.js +1 -0
- package/dist/src/store/turns/schema.js +102 -1
- package/dist/src/store/turns/sqlite-store/rows.js +23 -0
- package/dist/src/store/turns/sqlite-store/write.js +7 -3
- package/dist/src/topics/durability.js +187 -0
- package/dist/src/topics/index.js +2 -1
- package/dist/src/topics/store.js +28 -9
- package/dist/src/wiki/curation-helpers.js +71 -0
- package/dist/src/wiki/curation.js +147 -0
- package/dist/src/wiki/incremental.js +126 -0
- package/dist/src/wiki/index.js +5 -0
- package/extensions/dashboard-client/dist/assets/AreaChart-KAmp_6bL.js +70 -0
- package/extensions/dashboard-client/dist/assets/{SessionsTab-C30AwyoF.js.map → AreaChart-KAmp_6bL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/CacheTab-Bk-gm-R0.js +2 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Bk-gm-R0.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-Cyylo0of.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-Cyylo0of.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-D-IzmV29.js +2 -0
- package/extensions/dashboard-client/dist/assets/HealthTab-D-IzmV29.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-CrlkCFVj.js +2 -0
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-CrlkCFVj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DHUdZ7RR.js +2 -0
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DHUdZ7RR.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-B7I4HWVg.js +2 -0
- package/extensions/dashboard-client/dist/assets/MetricsTab-B7I4HWVg.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-K6j4ZI7u.js +2 -0
- package/extensions/dashboard-client/dist/assets/OverviewTab-K6j4ZI7u.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-BSRWhn56.js +2 -0
- package/extensions/dashboard-client/dist/assets/ReposTab-BSRWhn56.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-BXdizHN1.js +2 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-BXdizHN1.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-CAgyfqY3.js +2 -0
- package/extensions/dashboard-client/dist/assets/SetupTab-CAgyfqY3.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-DwgCzPD9.js +2 -0
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-DwgCzPD9.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CVKcStBp.js +5 -0
- package/extensions/dashboard-client/dist/assets/TurnsTab-CVKcStBp.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-BNohxTbs.js +2 -0
- package/extensions/dashboard-client/dist/assets/WikiTab-BNohxTbs.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/button-B4A-_dCA.js +2 -0
- package/extensions/dashboard-client/dist/assets/button-B4A-_dCA.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/card-Cjn7Ifoe.js +2 -0
- package/extensions/dashboard-client/dist/assets/card-Cjn7Ifoe.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CQjCLvv7.js +137 -0
- package/extensions/dashboard-client/dist/assets/index-CQjCLvv7.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +1 -0
- package/extensions/dashboard-client/dist/assets/switch-CcdXX0Zm.js +2 -0
- package/extensions/dashboard-client/dist/assets/switch-CcdXX0Zm.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/toggle-KdBnFLu3.js +2 -0
- package/extensions/dashboard-client/dist/assets/toggle-KdBnFLu3.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js → useSSE-B6t44vzc.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-p1pOI_-P.js.map → useSSE-B6t44vzc.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/package-lock.json +1995 -191
- package/extensions/dashboard-client/package.json +12 -1
- package/extensions/dashboard-client/postcss.config.js +6 -0
- package/extensions/dashboard-client/src/App.tsx +127 -88
- package/extensions/dashboard-client/src/api/client.ts +109 -10
- package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +15 -7
- package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +67 -55
- package/extensions/dashboard-client/src/components/CacheHitRateTrendCard.tsx +17 -13
- package/extensions/dashboard-client/src/components/CacheHitsCard.tsx +35 -30
- package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +33 -28
- package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +5 -6
- package/extensions/dashboard-client/src/components/EventStream.tsx +17 -13
- package/extensions/dashboard-client/src/components/HydeDetailPanel.tsx +173 -0
- package/extensions/dashboard-client/src/components/LegendCard.tsx +16 -9
- package/extensions/dashboard-client/src/components/PerfCards.tsx +28 -20
- package/extensions/dashboard-client/src/components/PerfChart.tsx +9 -6
- package/extensions/dashboard-client/src/components/ProviderCacheCard.tsx +15 -7
- package/extensions/dashboard-client/src/components/RagDashboard.tsx +253 -0
- package/extensions/dashboard-client/src/components/RepoAllSessionsCard.tsx +15 -10
- package/extensions/dashboard-client/src/components/RepoContextStack.tsx +24 -16
- package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +52 -49
- package/extensions/dashboard-client/src/components/RepoTable.tsx +26 -17
- package/extensions/dashboard-client/src/components/SessionContextGauges.tsx +11 -14
- package/extensions/dashboard-client/src/components/SessionInfo.tsx +20 -15
- package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +8 -5
- package/extensions/dashboard-client/src/components/SummaryTiles.tsx +26 -17
- package/extensions/dashboard-client/src/components/TabBar.tsx +1 -3
- package/extensions/dashboard-client/src/components/layout/AppShell.tsx +42 -0
- package/extensions/dashboard-client/src/components/layout/BottomBar.tsx +88 -0
- package/extensions/dashboard-client/src/components/layout/Sidebar.tsx +83 -0
- package/extensions/dashboard-client/src/components/ui/badge.tsx +30 -0
- package/extensions/dashboard-client/src/components/ui/button.tsx +44 -0
- package/extensions/dashboard-client/src/components/ui/card.tsx +32 -0
- package/extensions/dashboard-client/src/components/ui/select.tsx +64 -0
- package/extensions/dashboard-client/src/components/ui/switch.tsx +26 -0
- package/extensions/dashboard-client/src/components/ui/tabs.tsx +36 -0
- package/extensions/dashboard-client/src/components/ui/toggle.tsx +28 -0
- package/extensions/dashboard-client/src/components/ui/tooltip.tsx +27 -0
- package/extensions/dashboard-client/src/config.ts +10 -0
- package/extensions/dashboard-client/src/main.tsx +2 -2
- package/extensions/dashboard-client/src/memory-map-layout.ts +1 -2
- package/extensions/dashboard-client/src/styles/index.css +79 -0
- package/extensions/dashboard-client/src/tabs/CacheTab.tsx +33 -26
- package/extensions/dashboard-client/src/tabs/EventsTab.tsx +22 -7
- package/extensions/dashboard-client/src/tabs/HealthTab.tsx +77 -46
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/ActionsCard.tsx +60 -50
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DbStatsCard.tsx +61 -35
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/DebugBundleCard.tsx +34 -23
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/HealthMitigationCard.tsx +62 -43
- package/extensions/dashboard-client/src/tabs/MaintenanceTab/SchemaHealthCard.tsx +53 -32
- package/extensions/dashboard-client/src/tabs/MaintenanceTab.tsx +2 -2
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/MemoryMapView.tsx +29 -46
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +10 -34
- package/extensions/dashboard-client/src/tabs/MemoryMapTab.tsx +14 -34
- package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +5 -2
- package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +11 -5
- package/extensions/dashboard-client/src/tabs/ReposTab.tsx +5 -5
- package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +47 -33
- package/extensions/dashboard-client/src/tabs/SetupTab/EmbedderSetup.tsx +6 -2
- package/extensions/dashboard-client/src/tabs/SetupTab/RagHealthCard.tsx +78 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/SettingsPanel.tsx +198 -0
- package/extensions/dashboard-client/src/tabs/SetupTab/SettingsSection.tsx +50 -0
- package/extensions/dashboard-client/src/tabs/SetupTab.tsx +21 -59
- package/extensions/dashboard-client/src/tabs/TopicsTab.tsx +75 -40
- package/extensions/dashboard-client/src/tabs/TurnsTab.tsx +150 -112
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicEvolutionGraph.tsx +274 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicEvolutionView.tsx +71 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/TopicTimeline.tsx +103 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/WikiPage.tsx +213 -0
- package/extensions/dashboard-client/src/tabs/WikiTab/WikiPageControls.tsx +188 -0
- package/extensions/dashboard-client/src/tabs/WikiTab.tsx +222 -0
- package/extensions/dashboard-client/src/tabs/registry.ts +57 -0
- package/extensions/dashboard-client/src/utils/cn.ts +7 -0
- package/extensions/dashboard-client/tailwind.config.js +46 -0
- package/extensions/dashboard-server/api-contracts/core.ts +59 -0
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +79 -0
- package/extensions/dashboard-server/api-contracts/index.ts +48 -2
- package/extensions/dashboard-server/api-contracts/rag-metrics.ts +22 -0
- package/extensions/dashboard-server/api-contracts/rag-settings.ts +30 -20
- package/extensions/dashboard-server/api-contracts/wiki.ts +138 -0
- package/extensions/dashboard-server/routes-rag-metrics.ts +82 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +314 -0
- package/extensions/dashboard-server/routes-rag-settings.ts +147 -97
- package/extensions/dashboard-server/routes-wiki-helpers.ts +126 -0
- package/extensions/dashboard-server/routes-wiki.ts +376 -0
- package/extensions/dashboard-server/routes.ts +2 -0
- package/extensions/dashboard-server/server.ts +18 -2
- package/extensions/mega-events/context-handler/afterCompact.ts +44 -1
- package/extensions/mega-pipeline/recall.ts +31 -0
- package/extensions/mega-turn-store.ts +13 -0
- package/package.json +1 -1
- package/src/config/turns.ts +17 -0
- package/src/config.ts +3 -0
- package/src/recall/async.ts +10 -1
- package/src/recall/format.ts +15 -2
- package/src/recall/hydeTelemetry.ts +44 -0
- package/src/recall/sync.ts +34 -2
- package/src/recall/types.ts +36 -0
- package/src/store/turns/hydeStore.ts +82 -0
- package/src/store/turns/index.ts +4 -0
- package/src/store/turns/schema.ts +126 -1
- package/src/store/turns/sqlite-store/rows.ts +25 -0
- package/src/store/turns/sqlite-store/write.ts +20 -2
- package/src/store/turns/types.ts +33 -0
- package/src/topics/durability.ts +231 -0
- package/src/topics/index.ts +7 -1
- package/src/topics/store.ts +35 -9
- package/src/wiki/curation-helpers.ts +140 -0
- package/src/wiki/curation.ts +217 -0
- package/src/wiki/incremental.ts +164 -0
- package/src/wiki/index.ts +13 -0
- package/extensions/dashboard-client/dist/assets/CacheTab-Cqnc7lMg.js +0 -2
- package/extensions/dashboard-client/dist/assets/CacheTab-Cqnc7lMg.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/EventsTab-tYk3VFAp.js +0 -2
- package/extensions/dashboard-client/dist/assets/EventsTab-tYk3VFAp.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/HealthTab-D2_kjgxw.js +0 -2
- package/extensions/dashboard-client/dist/assets/HealthTab-D2_kjgxw.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Bc-uPYrz.js +0 -2
- package/extensions/dashboard-client/dist/assets/MaintenanceTab-Bc-uPYrz.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-D1V9g_NC.js +0 -2
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-D1V9g_NC.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/MetricsTab-DT_XfL96.js +0 -2
- package/extensions/dashboard-client/dist/assets/MetricsTab-DT_XfL96.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/OverviewTab-BY6KZCme.js +0 -2
- package/extensions/dashboard-client/dist/assets/OverviewTab-BY6KZCme.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/ReposTab-CeS-tpDM.js +0 -2
- package/extensions/dashboard-client/dist/assets/ReposTab-CeS-tpDM.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/SessionsTab-C30AwyoF.js +0 -70
- package/extensions/dashboard-client/dist/assets/SetupTab-BJUHOVK2.js +0 -2
- package/extensions/dashboard-client/dist/assets/SetupTab-BJUHOVK2.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-p55K1tFP.js +0 -2
- package/extensions/dashboard-client/dist/assets/TimeSavedCard-p55K1tFP.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TopicsTab-D8FqyAbK.js +0 -2
- package/extensions/dashboard-client/dist/assets/TopicsTab-D8FqyAbK.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/TurnsTab-ju-nXDys.js +0 -2
- package/extensions/dashboard-client/dist/assets/TurnsTab-ju-nXDys.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-BEj1Zahy.js +0 -42
- package/extensions/dashboard-client/dist/assets/index-BEj1Zahy.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/index-CUNn22iV.css +0 -1
- package/extensions/dashboard-client/src/styles/metrics-extra.css +0 -69
- package/extensions/dashboard-client/src/styles/turns.css +0 -142
- package/extensions/dashboard-client/src/tabs/SetupTab/RagSettingsCard.tsx +0 -160
|
@@ -30,17 +30,21 @@ import type {
|
|
|
30
30
|
TurnRow,
|
|
31
31
|
ConversationSummary,
|
|
32
32
|
} from "@contracts";
|
|
33
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
|
34
|
+
import { Badge } from "../components/ui/badge";
|
|
35
|
+
import { Button } from "../components/ui/button";
|
|
36
|
+
import { HydeDetailPanel } from "../components/HydeDetailPanel";
|
|
33
37
|
|
|
34
38
|
function fmtTs(ms: number): string {
|
|
35
39
|
if (!ms) return "—";
|
|
36
40
|
return new Date(ms).toLocaleString();
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
function
|
|
40
|
-
if (band === "green") return "
|
|
41
|
-
if (band === "yellow") return "
|
|
42
|
-
if (band === "red") return "
|
|
43
|
-
return "
|
|
43
|
+
function bandVariant(band: TurnRow["pressureBand"]): "success" | "warning" | "danger" | "default" {
|
|
44
|
+
if (band === "green") return "success";
|
|
45
|
+
if (band === "yellow") return "warning";
|
|
46
|
+
if (band === "red") return "danger";
|
|
47
|
+
return "default";
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
function sourceLabel(s: TurnRow["recall"][number]["source"]): string {
|
|
@@ -145,70 +149,88 @@ export default function TurnsTab(): React.ReactElement {
|
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
return (
|
|
148
|
-
<div className="
|
|
149
|
-
<h3
|
|
150
|
-
|
|
152
|
+
<div className="flex flex-col gap-4">
|
|
153
|
+
<h3 className="font-heading text-lg font-semibold">
|
|
154
|
+
Turn-by-turn memory tracking + recall
|
|
155
|
+
</h3>
|
|
156
|
+
{notice && <Badge variant="warning">{notice}</Badge>}
|
|
151
157
|
|
|
152
158
|
{intents && intents.intents.length > 0 && (
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
<Card>
|
|
160
|
+
<CardHeader>
|
|
161
|
+
<CardTitle>
|
|
162
|
+
Pending rewind intents ({intents.intents.length})
|
|
163
|
+
</CardTitle>
|
|
164
|
+
</CardHeader>
|
|
165
|
+
<CardContent>
|
|
166
|
+
<ul className="list-disc space-y-1 pl-5">
|
|
167
|
+
{intents.intents.map((i) => (
|
|
168
|
+
<li key={i.id}>
|
|
169
|
+
{fmtTs(i.createdAt)} — rewind{" "}
|
|
170
|
+
<code>{i.conversationId}</code> to turn {i.targetTurnIndex}
|
|
171
|
+
</li>
|
|
172
|
+
))}
|
|
173
|
+
</ul>
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
164
176
|
)}
|
|
165
177
|
|
|
166
|
-
<
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
178
|
+
<Card>
|
|
179
|
+
<CardHeader>
|
|
180
|
+
<CardTitle>
|
|
181
|
+
Conversations ({turns.conversations.length})
|
|
182
|
+
</CardTitle>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardContent>
|
|
185
|
+
<table className="w-full border-collapse text-sm">
|
|
186
|
+
<thead>
|
|
187
|
+
<tr>
|
|
188
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Conversation</th>
|
|
189
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Turns</th>
|
|
190
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Recall</th>
|
|
191
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Epochs</th>
|
|
192
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Avg ctx%</th>
|
|
193
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Last turn</th>
|
|
194
|
+
<th className="border-b border-border px-3 py-2"></th>
|
|
195
|
+
</tr>
|
|
196
|
+
</thead>
|
|
197
|
+
<tbody>
|
|
198
|
+
{turns.conversations.map((c: ConversationSummary) => (
|
|
199
|
+
<ConversationRow
|
|
200
|
+
key={c.conversationId}
|
|
201
|
+
c={c}
|
|
202
|
+
active={turns.activeConversationId === c.conversationId}
|
|
203
|
+
expanded={expanded === c.conversationId}
|
|
204
|
+
onExpand={() => expand(c.conversationId)}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</tbody>
|
|
208
|
+
</table>
|
|
209
|
+
</CardContent>
|
|
210
|
+
</Card>
|
|
193
211
|
|
|
194
212
|
{expanded && (
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
<Card>
|
|
214
|
+
<CardHeader>
|
|
215
|
+
<CardTitle>
|
|
216
|
+
Turns in <code>{expanded}</code>
|
|
217
|
+
</CardTitle>
|
|
218
|
+
</CardHeader>
|
|
219
|
+
<CardContent>
|
|
220
|
+
{detailLoading ? (
|
|
221
|
+
<div className="text-sm text-muted-foreground">Loading turns…</div>
|
|
222
|
+
) : detail ? (
|
|
223
|
+
<TurnDetail
|
|
224
|
+
detail={detail}
|
|
225
|
+
busy={busy}
|
|
226
|
+
onFork={onFork}
|
|
227
|
+
onRewind={onRewind}
|
|
228
|
+
/>
|
|
229
|
+
) : (
|
|
230
|
+
<div className="text-sm text-muted-foreground">No detail.</div>
|
|
231
|
+
)}
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
212
234
|
)}
|
|
213
235
|
</div>
|
|
214
236
|
);
|
|
@@ -228,20 +250,22 @@ function ConversationRow({
|
|
|
228
250
|
return (
|
|
229
251
|
<>
|
|
230
252
|
<tr
|
|
231
|
-
className={
|
|
253
|
+
className={`border-b border-border/50 hover:bg-bg-elevated/40 ${
|
|
254
|
+
active ? "bg-primary/10" : ""
|
|
255
|
+
}`}
|
|
232
256
|
onClick={onExpand}
|
|
233
257
|
style={{ cursor: "pointer" }}
|
|
234
258
|
>
|
|
235
|
-
<td>
|
|
259
|
+
<td className="px-3 py-2">
|
|
236
260
|
{expanded ? "▾" : "▸"} <code>{c.conversationId}</code>
|
|
237
|
-
{active && <
|
|
261
|
+
{active && <Badge variant="accent" className="ml-2">active</Badge>}
|
|
238
262
|
</td>
|
|
239
|
-
<td>{c.turnCount}</td>
|
|
240
|
-
<td>{c.totalRecall}</td>
|
|
241
|
-
<td>{c.epochCount}</td>
|
|
242
|
-
<td>{c.avgCtxPercent.toFixed(0)}%</td>
|
|
243
|
-
<td>{fmtTs(c.lastTurnAt)}</td>
|
|
244
|
-
<td />
|
|
263
|
+
<td className="px-3 py-2 text-right">{c.turnCount}</td>
|
|
264
|
+
<td className="px-3 py-2 text-right">{c.totalRecall}</td>
|
|
265
|
+
<td className="px-3 py-2 text-right">{c.epochCount}</td>
|
|
266
|
+
<td className="px-3 py-2 text-right">{c.avgCtxPercent.toFixed(0)}%</td>
|
|
267
|
+
<td className="px-3 py-2">{fmtTs(c.lastTurnAt)}</td>
|
|
268
|
+
<td className="px-3 py-2" />
|
|
245
269
|
</tr>
|
|
246
270
|
</>
|
|
247
271
|
);
|
|
@@ -259,48 +283,52 @@ function TurnDetail({
|
|
|
259
283
|
onRewind: (conversationId: string, turnIndex: number) => void;
|
|
260
284
|
}): React.ReactElement {
|
|
261
285
|
return (
|
|
262
|
-
<table className="
|
|
286
|
+
<table className="w-full border-collapse text-sm">
|
|
263
287
|
<thead>
|
|
264
288
|
<tr>
|
|
265
|
-
<th>#</th>
|
|
266
|
-
<th>Role</th>
|
|
267
|
-
<th>Ctx</th>
|
|
268
|
-
<th>Band</th>
|
|
269
|
-
<th>Epoch</th>
|
|
270
|
-
<th>Recalled checkpoints (memory recall)</th>
|
|
271
|
-
<th>
|
|
272
|
-
<th>
|
|
289
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">#</th>
|
|
290
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Role</th>
|
|
291
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Ctx</th>
|
|
292
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Band</th>
|
|
293
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Epoch</th>
|
|
294
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Recalled checkpoints (memory recall)</th>
|
|
295
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">HyDE</th>
|
|
296
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Ended</th>
|
|
297
|
+
<th className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">Actions</th>
|
|
273
298
|
</tr>
|
|
274
299
|
</thead>
|
|
275
300
|
<tbody>
|
|
276
301
|
{detail.turns.map((t) => (
|
|
277
|
-
<tr
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
302
|
+
<tr
|
|
303
|
+
key={`${t.conversationId}:${t.turnIndex}`}
|
|
304
|
+
className="border-b border-border/50 hover:bg-bg-elevated/40"
|
|
305
|
+
>
|
|
306
|
+
<td className="px-3 py-2 text-right">{t.turnIndex}</td>
|
|
307
|
+
<td className="px-3 py-2">{t.role}</td>
|
|
308
|
+
<td className="px-3 py-2">
|
|
281
309
|
{t.ctxTokens ?? "—"}
|
|
282
310
|
{t.ctxPercent != null && (
|
|
283
|
-
<span className="muted">
|
|
311
|
+
<span className="text-muted-foreground">
|
|
284
312
|
{" "}
|
|
285
313
|
· {t.ctxPercent.toFixed(0)}%
|
|
286
314
|
</span>
|
|
287
315
|
)}
|
|
288
316
|
</td>
|
|
289
|
-
<td>
|
|
290
|
-
<
|
|
317
|
+
<td className="px-3 py-2">
|
|
318
|
+
<Badge variant={bandVariant(t.pressureBand)}>
|
|
291
319
|
{t.pressureBand ?? "—"}
|
|
292
|
-
</
|
|
320
|
+
</Badge>
|
|
293
321
|
</td>
|
|
294
|
-
<td>{t.epochId ? <code>{t.epochId.slice(0, 8)}</code> : "—"}</td>
|
|
295
|
-
<td className="
|
|
322
|
+
<td className="px-3 py-2">{t.epochId ? <code>{t.epochId.slice(0, 8)}</code> : "—"}</td>
|
|
323
|
+
<td className="px-3 py-2">
|
|
296
324
|
{t.recall.length === 0 ? (
|
|
297
|
-
<span className="muted">—</span>
|
|
325
|
+
<span className="text-muted-foreground">—</span>
|
|
298
326
|
) : (
|
|
299
|
-
<ul className="
|
|
327
|
+
<ul className="space-y-1">
|
|
300
328
|
{t.recall.map((r) => (
|
|
301
329
|
<li key={r.checkpointId}>
|
|
302
330
|
<code>{r.checkpointId.slice(0, 12)}</code>{" "}
|
|
303
|
-
<span className="muted">
|
|
331
|
+
<span className="text-muted-foreground">
|
|
304
332
|
{sourceLabel(r.source)} · {r.score.toFixed(2)}
|
|
305
333
|
{r.raptorLevel != null ? ` · L${r.raptorLevel}` : ""}
|
|
306
334
|
</span>
|
|
@@ -309,24 +337,34 @@ function TurnDetail({
|
|
|
309
337
|
</ul>
|
|
310
338
|
)}
|
|
311
339
|
</td>
|
|
312
|
-
<td>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
340
|
+
<td className="px-3 py-2 align-top">
|
|
341
|
+
<HydeDetailPanel
|
|
342
|
+
conversationId={t.conversationId}
|
|
343
|
+
turnIndex={t.turnIndex}
|
|
344
|
+
/>
|
|
345
|
+
</td>
|
|
346
|
+
<td className="px-3 py-2">{fmtTs(t.endedAt)}</td>
|
|
347
|
+
<td className="px-3 py-2">
|
|
348
|
+
<div className="flex gap-2">
|
|
349
|
+
<Button
|
|
350
|
+
type="button"
|
|
351
|
+
variant="outline"
|
|
352
|
+
size="sm"
|
|
353
|
+
disabled={busy !== null}
|
|
354
|
+
onClick={() => onFork(t.conversationId, t.turnIndex)}
|
|
355
|
+
>
|
|
356
|
+
fork
|
|
357
|
+
</Button>
|
|
358
|
+
<Button
|
|
359
|
+
type="button"
|
|
360
|
+
variant="outline"
|
|
361
|
+
size="sm"
|
|
362
|
+
disabled={busy !== null}
|
|
363
|
+
onClick={() => onRewind(t.conversationId, t.turnIndex)}
|
|
364
|
+
>
|
|
365
|
+
rewind
|
|
366
|
+
</Button>
|
|
367
|
+
</div>
|
|
330
368
|
</td>
|
|
331
369
|
</tr>
|
|
332
370
|
))}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/tabs/WikiTab/TopicEvolutionGraph.tsx — D3 evolution.
|
|
3
|
+
*
|
|
4
|
+
* Force-directed graph of wiki topics over time. Nodes are topics (radius scales
|
|
5
|
+
* with memoryCount); edges are merge/split curation events. A bucket scrubber
|
|
6
|
+
* (from the response buckets) dims nodes that had no activity up to the selected
|
|
7
|
+
* time window — a lightweight "growth over time" view.
|
|
8
|
+
*
|
|
9
|
+
* Self-contained force simulation (Coulomb repulsion + Hooke attraction +
|
|
10
|
+
* center gravity) — the codebase deliberately avoids external graph libraries
|
|
11
|
+
* (see memory-map-layout.ts). No data is fabricated: only real node/edge/bucket
|
|
12
|
+
* fields from the contract are used. Styling is Tailwind.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type React from "react";
|
|
16
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
|
+
import type { TopicEvolutionResponse } from "@contracts";
|
|
18
|
+
|
|
19
|
+
// ── Simulation constants ────────────────────────────────────────────────
|
|
20
|
+
const W = 900;
|
|
21
|
+
const H = 560;
|
|
22
|
+
const REPULSION = 60_000;
|
|
23
|
+
const ATTRACTION = 0.012;
|
|
24
|
+
const CENTER_GRAVITY = 0.012;
|
|
25
|
+
const DAMPING = 0.8;
|
|
26
|
+
const SPEED_LIMIT = 2;
|
|
27
|
+
const MIN_VELOCITY = 0.02;
|
|
28
|
+
const MAX_ITERATIONS = 250;
|
|
29
|
+
|
|
30
|
+
interface SimNode {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
memoryCount: number;
|
|
34
|
+
pos: { x: number; y: number };
|
|
35
|
+
vx: number;
|
|
36
|
+
vy: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SimEdge {
|
|
40
|
+
source: number;
|
|
41
|
+
target: number;
|
|
42
|
+
kind: "merge" | "split";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function edgeColor(kind: "merge" | "split"): string {
|
|
46
|
+
return kind === "merge" ? "#f59e0b" : "#22c55e"; // amber / green
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initialLayout(nodes: TopicEvolutionResponse["nodes"]): SimNode[] {
|
|
50
|
+
return nodes.map((n, i) => {
|
|
51
|
+
const angle = (2 * Math.PI * i) / Math.max(nodes.length, 1);
|
|
52
|
+
return {
|
|
53
|
+
id: n.id,
|
|
54
|
+
label: n.label,
|
|
55
|
+
memoryCount: n.memoryCount,
|
|
56
|
+
pos: { x: Math.cos(angle) * 220 + W / 2, y: Math.sin(angle) * 220 + H / 2 },
|
|
57
|
+
vx: 0,
|
|
58
|
+
vy: 0,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function applyForces(sim: { nodes: SimNode[]; edges: SimEdge[] }): void {
|
|
64
|
+
const { nodes, edges } = sim;
|
|
65
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
66
|
+
const n = nodes[i];
|
|
67
|
+
let fx = 0;
|
|
68
|
+
let fy = 0;
|
|
69
|
+
for (let j = 0; j < nodes.length; j++) {
|
|
70
|
+
if (i === j) continue;
|
|
71
|
+
const dx = n.pos.x - nodes[j].pos.x;
|
|
72
|
+
const dy = n.pos.y - nodes[j].pos.y;
|
|
73
|
+
const distSq = dx * dx + dy * dy || 1;
|
|
74
|
+
const dist = Math.sqrt(distSq);
|
|
75
|
+
fx += (dx / dist) * (REPULSION / distSq);
|
|
76
|
+
fy += (dy / dist) * (REPULSION / distSq);
|
|
77
|
+
}
|
|
78
|
+
for (const e of edges) {
|
|
79
|
+
let other: number;
|
|
80
|
+
if (e.source === i) other = e.target;
|
|
81
|
+
else if (e.target === i) other = e.source;
|
|
82
|
+
else continue;
|
|
83
|
+
const dx = nodes[other].pos.x - n.pos.x;
|
|
84
|
+
const dy = nodes[other].pos.y - n.pos.y;
|
|
85
|
+
fx += dx * ATTRACTION;
|
|
86
|
+
fy += dy * ATTRACTION;
|
|
87
|
+
}
|
|
88
|
+
fx -= n.pos.x * CENTER_GRAVITY;
|
|
89
|
+
fy -= n.pos.y * CENTER_GRAVITY;
|
|
90
|
+
n.vx = (n.vx + fx) * DAMPING;
|
|
91
|
+
n.vy = (n.vy + fy) * DAMPING;
|
|
92
|
+
const speed = Math.sqrt(n.vx * n.vx + n.vy * n.vy);
|
|
93
|
+
if (speed > SPEED_LIMIT) {
|
|
94
|
+
n.vx = (n.vx / speed) * SPEED_LIMIT;
|
|
95
|
+
n.vy = (n.vy / speed) * SPEED_LIMIT;
|
|
96
|
+
}
|
|
97
|
+
if (speed > MIN_VELOCITY) {
|
|
98
|
+
n.pos.x += n.vx;
|
|
99
|
+
n.pos.y += n.vy;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Props {
|
|
105
|
+
data: TopicEvolutionResponse;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function TopicEvolutionGraph({
|
|
109
|
+
data,
|
|
110
|
+
}: Props): React.ReactElement {
|
|
111
|
+
const simRef = useRef<{
|
|
112
|
+
nodes: SimNode[];
|
|
113
|
+
edges: SimEdge[];
|
|
114
|
+
timeByActivity: Map<string, number> | null;
|
|
115
|
+
} | null>(null);
|
|
116
|
+
const animRef = useRef(0);
|
|
117
|
+
const [, setFrame] = useState(0);
|
|
118
|
+
const [scrub, setScrub] = useState<number>(0);
|
|
119
|
+
|
|
120
|
+
// Initialize simulation from data (re-run when data changes).
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const nodes = initialLayout(data.nodes);
|
|
123
|
+
const indexById = new Map(nodes.map((n, i) => [n.id, i]));
|
|
124
|
+
const edges: SimEdge[] = data.edges
|
|
125
|
+
.map((e) => {
|
|
126
|
+
const si = indexById.get(e.source);
|
|
127
|
+
const ti = indexById.get(e.target);
|
|
128
|
+
if (si === undefined || ti === undefined || si === ti) return null;
|
|
129
|
+
return { source: si, target: ti, kind: e.kind };
|
|
130
|
+
})
|
|
131
|
+
.filter((e): e is SimEdge => e !== null);
|
|
132
|
+
// Latest activity per node (from merge/split edge timestamps).
|
|
133
|
+
const timeByActivity = new Map<string, number>();
|
|
134
|
+
for (const e of data.edges) {
|
|
135
|
+
timeByActivity.set(e.source, Math.max(timeByActivity.get(e.source) ?? 0, e.at));
|
|
136
|
+
timeByActivity.set(e.target, Math.max(timeByActivity.get(e.target) ?? 0, e.at));
|
|
137
|
+
}
|
|
138
|
+
simRef.current = { nodes, edges, timeByActivity };
|
|
139
|
+
setFrame(1);
|
|
140
|
+
return () => cancelAnimationFrame(animRef.current);
|
|
141
|
+
}, [data]);
|
|
142
|
+
|
|
143
|
+
// Run the force simulation to stable layout.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!simRef.current) return;
|
|
146
|
+
let iter = 0;
|
|
147
|
+
const tick = () => {
|
|
148
|
+
if (!simRef.current || iter >= MAX_ITERATIONS) return;
|
|
149
|
+
applyForces(simRef.current);
|
|
150
|
+
iter++;
|
|
151
|
+
setFrame(iter);
|
|
152
|
+
animRef.current = requestAnimationFrame(tick);
|
|
153
|
+
};
|
|
154
|
+
animRef.current = requestAnimationFrame(tick);
|
|
155
|
+
return () => cancelAnimationFrame(animRef.current);
|
|
156
|
+
}, [data]);
|
|
157
|
+
|
|
158
|
+
const sim = simRef.current;
|
|
159
|
+
|
|
160
|
+
const scrubMax = Math.max(data.buckets.length - 1, 0);
|
|
161
|
+
const scrubBucket = useMemo(() => {
|
|
162
|
+
if (data.buckets.length === 0) return null;
|
|
163
|
+
const idx = Math.min(scrub, data.buckets.length - 1);
|
|
164
|
+
return data.buckets[idx].bucket;
|
|
165
|
+
}, [scrub, data.buckets]);
|
|
166
|
+
|
|
167
|
+
if (!sim || data.nodes.length === 0) {
|
|
168
|
+
return (
|
|
169
|
+
<div className="rounded-lg border border-border bg-bg-card p-6 text-sm text-muted-foreground">
|
|
170
|
+
No evolution data yet. Merge or split topics to see connections, then
|
|
171
|
+
check back after a rebuild.
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const activeSet =
|
|
177
|
+
scrubBucket == null || !sim.timeByActivity
|
|
178
|
+
? null
|
|
179
|
+
: new Set(
|
|
180
|
+
[...sim.timeByActivity.entries()]
|
|
181
|
+
.filter(([, t]) => t <= scrubBucket)
|
|
182
|
+
.map(([id]) => id),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex flex-col gap-3">
|
|
187
|
+
{/* Scrubber */}
|
|
188
|
+
{data.buckets.length > 1 && (
|
|
189
|
+
<div className="flex items-center gap-3">
|
|
190
|
+
<input
|
|
191
|
+
type="range"
|
|
192
|
+
min={0}
|
|
193
|
+
max={scrubMax}
|
|
194
|
+
value={scrub}
|
|
195
|
+
onChange={(e) => setScrub(Number(e.target.value))}
|
|
196
|
+
className="w-full accent-primary"
|
|
197
|
+
aria-label="Scrub through topic activity time"
|
|
198
|
+
/>
|
|
199
|
+
<span className="whitespace-nowrap text-xs text-muted-foreground">
|
|
200
|
+
{scrubBucket ? new Date(scrubBucket).toLocaleDateString() : "all"}
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* SVG graph */}
|
|
206
|
+
<svg
|
|
207
|
+
width="100%"
|
|
208
|
+
height={H}
|
|
209
|
+
viewBox={`0 0 ${W} ${H}`}
|
|
210
|
+
role="img"
|
|
211
|
+
aria-label="Topic evolution graph"
|
|
212
|
+
className="rounded-lg border border-border/60 bg-bg-card"
|
|
213
|
+
>
|
|
214
|
+
{sim.edges.map((e, i) => {
|
|
215
|
+
const a = sim.nodes[e.source];
|
|
216
|
+
const b = sim.nodes[e.target];
|
|
217
|
+
return (
|
|
218
|
+
<line
|
|
219
|
+
key={`edge-${i}`}
|
|
220
|
+
x1={a.pos.x}
|
|
221
|
+
y1={a.pos.y}
|
|
222
|
+
x2={b.pos.x}
|
|
223
|
+
y2={b.pos.y}
|
|
224
|
+
stroke={edgeColor(e.kind)}
|
|
225
|
+
strokeWidth={1.5}
|
|
226
|
+
strokeDasharray={e.kind === "split" ? "4 3" : undefined}
|
|
227
|
+
opacity={0.5}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
{sim.nodes.map((n, i) => {
|
|
232
|
+
const active =
|
|
233
|
+
activeSet == null || activeSet.size === 0 || activeSet.has(n.id);
|
|
234
|
+
const r = Math.min(32, 8 + n.memoryCount * 3);
|
|
235
|
+
return (
|
|
236
|
+
<g key={`node-${i}`} opacity={active ? 1 : 0.15}>
|
|
237
|
+
<circle
|
|
238
|
+
cx={n.pos.x}
|
|
239
|
+
cy={n.pos.y}
|
|
240
|
+
r={r}
|
|
241
|
+
fill="hsl(var(--primary) / 0.18)"
|
|
242
|
+
stroke="hsl(var(--primary))"
|
|
243
|
+
strokeWidth={1.5}
|
|
244
|
+
/>
|
|
245
|
+
<text
|
|
246
|
+
x={n.pos.x}
|
|
247
|
+
y={n.pos.y + 4}
|
|
248
|
+
textAnchor="middle"
|
|
249
|
+
fontSize="10px"
|
|
250
|
+
fill="hsl(var(--foreground))"
|
|
251
|
+
className="pointer-events-none"
|
|
252
|
+
>
|
|
253
|
+
{n.label.length > 16 ? n.label.slice(0, 15) + "…" : n.label}
|
|
254
|
+
</text>
|
|
255
|
+
</g>
|
|
256
|
+
);
|
|
257
|
+
})}
|
|
258
|
+
</svg>
|
|
259
|
+
|
|
260
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
261
|
+
<span className="flex items-center gap-1.5">
|
|
262
|
+
<span className="inline-block h-0.5 w-6 bg-amber-500" /> merge
|
|
263
|
+
</span>
|
|
264
|
+
<span className="flex items-center gap-1.5">
|
|
265
|
+
<span className="inline-block h-0.5 w-6 border-t border-dashed border-green-500" />
|
|
266
|
+
split
|
|
267
|
+
</span>
|
|
268
|
+
<span>
|
|
269
|
+
{data.nodes.length} nodes · {data.edges.length} curation events
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dashboard-client/src/tabs/WikiTab/TopicEvolutionView.tsx — planet evolution.
|
|
3
|
+
*
|
|
4
|
+
* Thin shell over TopicEvolutionGraph: fetches GET /api/wiki/evolution via
|
|
5
|
+
* useApi and refetches when a `wiki_rebuilt` SSE event lands (emitted by the
|
|
6
|
+
* extension after a topic-model rebuild), so the graph + scrubber stay live
|
|
7
|
+
* between polls. No data is fabricated — only the contract fields are used.
|
|
8
|
+
*
|
|
9
|
+
* Styling: Tailwind + shadcn (Card). No legacy CSS classes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type React from "react";
|
|
13
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
14
|
+
import { useApi } from "../../hooks/useApi";
|
|
15
|
+
import { useSSE } from "../../hooks/useSSE";
|
|
16
|
+
import { fetchTopicEvolution } from "../../api/client";
|
|
17
|
+
import type { TopicEvolutionResponse, SseEvent } from "@contracts";
|
|
18
|
+
import { Card, CardContent } from "../../components/ui/card";
|
|
19
|
+
import TopicEvolutionGraph from "./TopicEvolutionGraph";
|
|
20
|
+
|
|
21
|
+
function isWikiRebuilt(e: SseEvent): e is Extract<SseEvent, { type: "wiki_rebuilt" }> {
|
|
22
|
+
return e.type === "wiki_rebuilt";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function TopicEvolutionView(): React.ReactElement {
|
|
26
|
+
const { data, error, loading, refetch } = useApi<TopicEvolutionResponse>(
|
|
27
|
+
useCallback(() => fetchTopicEvolution(), []),
|
|
28
|
+
{ pollInterval: 60_000 },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const { events } = useSSE();
|
|
32
|
+
const lastRebuiltTs = useMemo(() => {
|
|
33
|
+
const rebuilt = events.filter(isWikiRebuilt);
|
|
34
|
+
if (rebuilt.length === 0) return null;
|
|
35
|
+
return rebuilt[rebuilt.length - 1].ts;
|
|
36
|
+
}, [events]);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (lastRebuiltTs != null) refetch();
|
|
40
|
+
}, [lastRebuiltTs, refetch]);
|
|
41
|
+
|
|
42
|
+
if (error && !data) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="rounded-lg border border-border bg-bg-card p-6 text-sm text-danger">
|
|
45
|
+
Error loading evolution graph: {error.message}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (loading && !data) {
|
|
50
|
+
return (
|
|
51
|
+
<div className="rounded-lg border border-border bg-bg-card p-6 text-sm text-muted-foreground">
|
|
52
|
+
Loading evolution graph…
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Card>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<h3 className="mb-2 font-heading text-sm font-semibold text-muted-foreground">
|
|
61
|
+
Topic evolution
|
|
62
|
+
</h3>
|
|
63
|
+
{data ? (
|
|
64
|
+
<TopicEvolutionGraph data={data} />
|
|
65
|
+
) : (
|
|
66
|
+
<p className="text-sm text-muted-foreground">No data yet.</p>
|
|
67
|
+
)}
|
|
68
|
+
</CardContent>
|
|
69
|
+
</Card>
|
|
70
|
+
);
|
|
71
|
+
}
|