@tokentop/ttop 0.2.2 → 0.4.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 +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +42 -38
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +68 -57
- package/src/plugins/registry.ts +80 -71
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/retention.ts +146 -0
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +314 -183
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +164 -151
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +7 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
isDatabaseInitialized,
|
|
5
|
+
queryModelDailyCosts,
|
|
6
|
+
queryProjectDailyCosts,
|
|
7
|
+
queryProviderDailyCosts,
|
|
8
|
+
queryUsageTimeSeries,
|
|
9
|
+
} from "@/storage/index.ts";
|
|
10
|
+
import { useDemoMode } from "../contexts/DemoModeContext.tsx";
|
|
11
|
+
import { useStorageReady } from "../contexts/StorageContext.tsx";
|
|
12
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
7
13
|
|
|
8
14
|
// ============================================================================
|
|
9
15
|
// Types
|
|
10
16
|
// ============================================================================
|
|
11
17
|
|
|
12
|
-
type TimePeriod =
|
|
13
|
-
type MetricType =
|
|
14
|
-
type BreakdownDimension =
|
|
18
|
+
type TimePeriod = "7d" | "30d" | "90d";
|
|
19
|
+
type MetricType = "cost" | "tokens" | "requests";
|
|
20
|
+
type BreakdownDimension = "provider" | "model" | "project" | "off";
|
|
15
21
|
|
|
16
22
|
interface ChartPoint {
|
|
17
23
|
label: string;
|
|
@@ -46,17 +52,29 @@ interface PeriodSummary {
|
|
|
46
52
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
47
53
|
|
|
48
54
|
const METRIC_LABELS: Record<MetricType, string> = {
|
|
49
|
-
cost:
|
|
50
|
-
tokens:
|
|
51
|
-
requests:
|
|
55
|
+
cost: "COST",
|
|
56
|
+
tokens: "TOKENS",
|
|
57
|
+
requests: "REQUESTS",
|
|
52
58
|
};
|
|
53
59
|
|
|
54
60
|
const CHART_CHARS = {
|
|
55
|
-
h:
|
|
61
|
+
h: "─",
|
|
62
|
+
v: "│",
|
|
63
|
+
tl: "╭",
|
|
64
|
+
tr: "╮",
|
|
65
|
+
bl: "╰",
|
|
66
|
+
br: "╯",
|
|
67
|
+
cross: "┼",
|
|
68
|
+
t_l: "┤",
|
|
56
69
|
};
|
|
57
70
|
|
|
58
71
|
const GHOST_CHARS = {
|
|
59
|
-
h:
|
|
72
|
+
h: "╌",
|
|
73
|
+
v: "╎",
|
|
74
|
+
tl: "╭",
|
|
75
|
+
tr: "╮",
|
|
76
|
+
bl: "╰",
|
|
77
|
+
br: "╯",
|
|
60
78
|
};
|
|
61
79
|
|
|
62
80
|
// ============================================================================
|
|
@@ -84,53 +102,63 @@ function fmtNumber(val: number): string {
|
|
|
84
102
|
|
|
85
103
|
function fmtMetricValue(val: number, metric: MetricType): string {
|
|
86
104
|
switch (metric) {
|
|
87
|
-
case
|
|
88
|
-
|
|
89
|
-
case
|
|
105
|
+
case "cost":
|
|
106
|
+
return fmtCurrency(val);
|
|
107
|
+
case "tokens":
|
|
108
|
+
return fmtTokens(val);
|
|
109
|
+
case "requests":
|
|
110
|
+
return fmtNumber(val);
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
113
|
|
|
93
114
|
function fmtYAxisLabel(val: number, metric: MetricType): string {
|
|
94
115
|
switch (metric) {
|
|
95
|
-
case
|
|
96
|
-
|
|
97
|
-
case
|
|
116
|
+
case "cost":
|
|
117
|
+
return `$${Math.round(val)}`.padStart(5);
|
|
118
|
+
case "tokens":
|
|
119
|
+
return fmtTokens(val).padStart(5);
|
|
120
|
+
case "requests":
|
|
121
|
+
return `${Math.round(val)}`.padStart(5);
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
124
|
|
|
101
|
-
function fmtDeltaPct(
|
|
125
|
+
function fmtDeltaPct(
|
|
126
|
+
current: number,
|
|
127
|
+
previous: number,
|
|
128
|
+
hasPreviousData = true,
|
|
129
|
+
): { text: string; positive: boolean } {
|
|
102
130
|
if (!hasPreviousData) {
|
|
103
|
-
return { text:
|
|
131
|
+
return { text: "—", positive: true };
|
|
104
132
|
}
|
|
105
133
|
if (previous === 0) {
|
|
106
|
-
return current > 0 ? { text:
|
|
134
|
+
return current > 0 ? { text: "▲ new", positive: false } : { text: "── 0%", positive: true };
|
|
107
135
|
}
|
|
108
136
|
const pct = ((current - previous) / previous) * 100;
|
|
109
|
-
const sign = pct >= 0 ?
|
|
137
|
+
const sign = pct >= 0 ? "▲" : "▼";
|
|
110
138
|
return { text: `${sign} ${Math.abs(pct).toFixed(0)}%`, positive: pct <= 0 };
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
function miniSparkline(values: number[], width: number): string {
|
|
114
|
-
if (values.length === 0) return
|
|
142
|
+
if (values.length === 0) return " ".repeat(width);
|
|
115
143
|
const max = Math.max(...values, 0.001);
|
|
116
|
-
const blocks =
|
|
144
|
+
const blocks = "▁▂▃▄▅▆▇█";
|
|
117
145
|
const step = Math.max(1, values.length / width);
|
|
118
|
-
let result =
|
|
146
|
+
let result = "";
|
|
119
147
|
for (let i = 0; i < width; i++) {
|
|
120
148
|
const idx = Math.min(Math.floor(i * step), values.length - 1);
|
|
121
149
|
const v = values[idx] ?? 0;
|
|
122
150
|
const normalized = Math.min(7, Math.floor((v / max) * 8));
|
|
123
|
-
result += v <= 0 ?
|
|
151
|
+
result += v <= 0 ? "▁" : (blocks[normalized] ?? "▁");
|
|
124
152
|
}
|
|
125
153
|
return result;
|
|
126
154
|
}
|
|
127
155
|
|
|
128
156
|
function padRight(str: string, len: number): string {
|
|
129
|
-
return str.length >= len ? str.slice(0, len) : str +
|
|
157
|
+
return str.length >= len ? str.slice(0, len) : str + " ".repeat(len - str.length);
|
|
130
158
|
}
|
|
131
159
|
|
|
132
160
|
function truncate(str: string, maxLen: number): string {
|
|
133
|
-
return str.length <= maxLen ? str : str.slice(0, maxLen - 1) +
|
|
161
|
+
return str.length <= maxLen ? str : str.slice(0, maxLen - 1) + "…";
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
// ============================================================================
|
|
@@ -138,24 +166,27 @@ function truncate(str: string, maxLen: number): string {
|
|
|
138
166
|
// ============================================================================
|
|
139
167
|
|
|
140
168
|
function getDaysBack(period: TimePeriod): number {
|
|
141
|
-
return period ===
|
|
169
|
+
return period === "30d" ? 30 : period === "90d" ? 90 : 7;
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
function formatDateLabel(date: Date, period: TimePeriod, index: number): string {
|
|
145
|
-
if (period ===
|
|
146
|
-
if (period ===
|
|
147
|
-
return index % 15 === 0 ? `${date.getUTCMonth() + 1}/${date.getUTCDate()}` :
|
|
173
|
+
if (period === "7d") return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
174
|
+
if (period === "30d") return index % 5 === 0 ? date.getUTCDate().toString() : "";
|
|
175
|
+
return index % 15 === 0 ? `${date.getUTCMonth() + 1}/${date.getUTCDate()}` : "";
|
|
148
176
|
}
|
|
149
177
|
|
|
150
178
|
function getDayName(date: Date): string {
|
|
151
|
-
return date.toLocaleDateString(
|
|
179
|
+
return date.toLocaleDateString("en-US", { weekday: "short" });
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
function getMetricValue(point: ChartPoint, metric: MetricType): number {
|
|
155
183
|
switch (metric) {
|
|
156
|
-
case
|
|
157
|
-
|
|
158
|
-
case
|
|
184
|
+
case "cost":
|
|
185
|
+
return point.costUsd;
|
|
186
|
+
case "tokens":
|
|
187
|
+
return point.tokens;
|
|
188
|
+
case "requests":
|
|
189
|
+
return point.requestCount;
|
|
159
190
|
}
|
|
160
191
|
}
|
|
161
192
|
|
|
@@ -163,7 +194,8 @@ function computeSummary(points: ChartPoint[], metric: MetricType): PeriodSummary
|
|
|
163
194
|
const totalCost = points.reduce((sum, p) => sum + p.costUsd, 0);
|
|
164
195
|
const totalTokens = points.reduce((sum, p) => sum + p.tokens, 0);
|
|
165
196
|
const totalRequests = points.reduce((sum, p) => sum + p.requestCount, 0);
|
|
166
|
-
const metricTotal =
|
|
197
|
+
const metricTotal =
|
|
198
|
+
metric === "tokens" ? totalTokens : metric === "requests" ? totalRequests : totalCost;
|
|
167
199
|
const avgPerDay = points.length > 0 ? metricTotal / points.length : 0;
|
|
168
200
|
|
|
169
201
|
let peakDay: { label: string; value: number } | null = null;
|
|
@@ -231,19 +263,30 @@ function fetchProviderBreakdown(period: TimePeriod): ProviderContribution[] {
|
|
|
231
263
|
try {
|
|
232
264
|
const rows = queryProviderDailyCosts(startMs, now, MS_PER_DAY);
|
|
233
265
|
|
|
234
|
-
const providerMap = new Map<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
266
|
+
const providerMap = new Map<
|
|
267
|
+
string,
|
|
268
|
+
{
|
|
269
|
+
cost: number;
|
|
270
|
+
tokens: number;
|
|
271
|
+
requests: number;
|
|
272
|
+
dailyCosts: Map<number, number>;
|
|
273
|
+
}
|
|
274
|
+
>();
|
|
238
275
|
|
|
239
276
|
for (const row of rows) {
|
|
240
277
|
const entry = providerMap.get(row.provider) ?? {
|
|
241
|
-
cost: 0,
|
|
278
|
+
cost: 0,
|
|
279
|
+
tokens: 0,
|
|
280
|
+
requests: 0,
|
|
281
|
+
dailyCosts: new Map(),
|
|
242
282
|
};
|
|
243
283
|
entry.cost += row.costUsd;
|
|
244
284
|
entry.tokens += row.tokens;
|
|
245
285
|
entry.requests += row.requestCount;
|
|
246
|
-
entry.dailyCosts.set(
|
|
286
|
+
entry.dailyCosts.set(
|
|
287
|
+
row.bucketStart,
|
|
288
|
+
(entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
|
|
289
|
+
);
|
|
247
290
|
providerMap.set(row.provider, entry);
|
|
248
291
|
}
|
|
249
292
|
|
|
@@ -283,19 +326,30 @@ function fetchModelBreakdown(period: TimePeriod): ProviderContribution[] {
|
|
|
283
326
|
try {
|
|
284
327
|
const rows = queryModelDailyCosts(startMs, now, MS_PER_DAY);
|
|
285
328
|
|
|
286
|
-
const modelMap = new Map<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
329
|
+
const modelMap = new Map<
|
|
330
|
+
string,
|
|
331
|
+
{
|
|
332
|
+
cost: number;
|
|
333
|
+
tokens: number;
|
|
334
|
+
requests: number;
|
|
335
|
+
dailyCosts: Map<number, number>;
|
|
336
|
+
}
|
|
337
|
+
>();
|
|
290
338
|
|
|
291
339
|
for (const row of rows) {
|
|
292
340
|
const entry = modelMap.get(row.model) ?? {
|
|
293
|
-
cost: 0,
|
|
341
|
+
cost: 0,
|
|
342
|
+
tokens: 0,
|
|
343
|
+
requests: 0,
|
|
344
|
+
dailyCosts: new Map(),
|
|
294
345
|
};
|
|
295
346
|
entry.cost += row.costUsd;
|
|
296
347
|
entry.tokens += row.tokens;
|
|
297
348
|
entry.requests += row.requestCount;
|
|
298
|
-
entry.dailyCosts.set(
|
|
349
|
+
entry.dailyCosts.set(
|
|
350
|
+
row.bucketStart,
|
|
351
|
+
(entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
|
|
352
|
+
);
|
|
299
353
|
modelMap.set(row.model, entry);
|
|
300
354
|
}
|
|
301
355
|
|
|
@@ -335,20 +389,31 @@ function fetchProjectBreakdown(period: TimePeriod): ProviderContribution[] {
|
|
|
335
389
|
try {
|
|
336
390
|
const rows = queryProjectDailyCosts(startMs, now, MS_PER_DAY);
|
|
337
391
|
|
|
338
|
-
const projectMap = new Map<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
392
|
+
const projectMap = new Map<
|
|
393
|
+
string,
|
|
394
|
+
{
|
|
395
|
+
cost: number;
|
|
396
|
+
tokens: number;
|
|
397
|
+
requests: number;
|
|
398
|
+
dailyCosts: Map<number, number>;
|
|
399
|
+
}
|
|
400
|
+
>();
|
|
342
401
|
|
|
343
402
|
for (const row of rows) {
|
|
344
|
-
const displayName = row.projectPath.split(
|
|
403
|
+
const displayName = row.projectPath.split("/").pop() ?? row.projectPath;
|
|
345
404
|
const entry = projectMap.get(displayName) ?? {
|
|
346
|
-
cost: 0,
|
|
405
|
+
cost: 0,
|
|
406
|
+
tokens: 0,
|
|
407
|
+
requests: 0,
|
|
408
|
+
dailyCosts: new Map(),
|
|
347
409
|
};
|
|
348
410
|
entry.cost += row.costUsd;
|
|
349
411
|
entry.tokens += row.tokens;
|
|
350
412
|
entry.requests += row.requestCount;
|
|
351
|
-
entry.dailyCosts.set(
|
|
413
|
+
entry.dailyCosts.set(
|
|
414
|
+
row.bucketStart,
|
|
415
|
+
(entry.dailyCosts.get(row.bucketStart) ?? 0) + row.costUsd,
|
|
416
|
+
);
|
|
352
417
|
projectMap.set(displayName, entry);
|
|
353
418
|
}
|
|
354
419
|
|
|
@@ -383,19 +448,16 @@ function fetchProjectBreakdown(period: TimePeriod): ProviderContribution[] {
|
|
|
383
448
|
// ============================================================================
|
|
384
449
|
|
|
385
450
|
function fetchDemoTimeSeries(
|
|
386
|
-
simulator: NonNullable<ReturnType<typeof useDemoMode>[
|
|
451
|
+
simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
|
|
387
452
|
period: TimePeriod,
|
|
388
|
-
offsetPeriods = 0
|
|
453
|
+
offsetPeriods = 0,
|
|
389
454
|
): ChartPoint[] {
|
|
390
455
|
const daysBack = getDaysBack(period);
|
|
391
456
|
const totalDays = daysBack * (1 + offsetPeriods);
|
|
392
457
|
const historicalData = simulator.generateHistoricalCostData(totalDays);
|
|
393
458
|
|
|
394
459
|
const sliceEnd = historicalData.length - offsetPeriods * daysBack;
|
|
395
|
-
const periodData = historicalData.slice(
|
|
396
|
-
Math.max(0, sliceEnd - daysBack),
|
|
397
|
-
sliceEnd
|
|
398
|
-
);
|
|
460
|
+
const periodData = historicalData.slice(Math.max(0, sliceEnd - daysBack), sliceEnd);
|
|
399
461
|
|
|
400
462
|
return periodData.map((item, index) => {
|
|
401
463
|
const d = new Date(item.date);
|
|
@@ -412,15 +474,20 @@ function fetchDemoTimeSeries(
|
|
|
412
474
|
}
|
|
413
475
|
|
|
414
476
|
function fetchDemoProviderBreakdown(
|
|
415
|
-
simulator: NonNullable<ReturnType<typeof useDemoMode>[
|
|
416
|
-
period: TimePeriod
|
|
477
|
+
simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
|
|
478
|
+
period: TimePeriod,
|
|
417
479
|
): ProviderContribution[] {
|
|
418
480
|
const daysBack = getDaysBack(period);
|
|
419
481
|
const byProvider = simulator.generateHistoricalCostDataByProvider(daysBack);
|
|
420
482
|
|
|
421
|
-
const providerMap = new Map<
|
|
422
|
-
|
|
423
|
-
|
|
483
|
+
const providerMap = new Map<
|
|
484
|
+
string,
|
|
485
|
+
{
|
|
486
|
+
cost: number;
|
|
487
|
+
tokens: number;
|
|
488
|
+
requests: number;
|
|
489
|
+
}
|
|
490
|
+
>();
|
|
424
491
|
const providerDayMap = new Map<string, Map<number, number>>();
|
|
425
492
|
|
|
426
493
|
for (const entry of byProvider) {
|
|
@@ -430,7 +497,9 @@ function fetchDemoProviderBreakdown(
|
|
|
430
497
|
providerDayMap.get(entry.provider)!.set(entry.date, entry.cost);
|
|
431
498
|
|
|
432
499
|
const existing = providerMap.get(entry.provider) ?? {
|
|
433
|
-
cost: 0,
|
|
500
|
+
cost: 0,
|
|
501
|
+
tokens: 0,
|
|
502
|
+
requests: 0,
|
|
434
503
|
};
|
|
435
504
|
existing.cost += entry.cost;
|
|
436
505
|
existing.tokens += entry.tokens;
|
|
@@ -439,12 +508,12 @@ function fetchDemoProviderBreakdown(
|
|
|
439
508
|
}
|
|
440
509
|
|
|
441
510
|
const totalCost = Array.from(providerMap.values()).reduce((sum, p) => sum + p.cost, 0);
|
|
442
|
-
const allDates = [...new Set(byProvider.map(e => e.date))].sort((a, b) => a - b);
|
|
511
|
+
const allDates = [...new Set(byProvider.map((e) => e.date))].sort((a, b) => a - b);
|
|
443
512
|
|
|
444
513
|
return Array.from(providerMap.entries())
|
|
445
514
|
.map(([provider, d]) => {
|
|
446
515
|
const dayMap = providerDayMap.get(provider) ?? new Map();
|
|
447
|
-
const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
|
|
516
|
+
const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
|
|
448
517
|
return {
|
|
449
518
|
provider,
|
|
450
519
|
cost: d.cost,
|
|
@@ -459,8 +528,8 @@ function fetchDemoProviderBreakdown(
|
|
|
459
528
|
}
|
|
460
529
|
|
|
461
530
|
function fetchDemoModelBreakdown(
|
|
462
|
-
simulator: NonNullable<ReturnType<typeof useDemoMode>[
|
|
463
|
-
period: TimePeriod
|
|
531
|
+
simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
|
|
532
|
+
period: TimePeriod,
|
|
464
533
|
): ProviderContribution[] {
|
|
465
534
|
const daysBack = getDaysBack(period);
|
|
466
535
|
const byModel = simulator.generateHistoricalCostDataByModel(daysBack);
|
|
@@ -480,12 +549,12 @@ function fetchDemoModelBreakdown(
|
|
|
480
549
|
}
|
|
481
550
|
|
|
482
551
|
const totalCost = Array.from(modelMap.values()).reduce((sum, p) => sum + p.cost, 0);
|
|
483
|
-
const allDates = [...new Set(byModel.map(e => e.date))].sort((a, b) => a - b);
|
|
552
|
+
const allDates = [...new Set(byModel.map((e) => e.date))].sort((a, b) => a - b);
|
|
484
553
|
|
|
485
554
|
return Array.from(modelMap.entries())
|
|
486
555
|
.map(([model, d]) => {
|
|
487
556
|
const dayMap = modelDayMap.get(model) ?? new Map();
|
|
488
|
-
const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
|
|
557
|
+
const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
|
|
489
558
|
return {
|
|
490
559
|
provider: model,
|
|
491
560
|
cost: d.cost,
|
|
@@ -500,8 +569,8 @@ function fetchDemoModelBreakdown(
|
|
|
500
569
|
}
|
|
501
570
|
|
|
502
571
|
function fetchDemoProjectBreakdown(
|
|
503
|
-
simulator: NonNullable<ReturnType<typeof useDemoMode>[
|
|
504
|
-
period: TimePeriod
|
|
572
|
+
simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
|
|
573
|
+
period: TimePeriod,
|
|
505
574
|
): ProviderContribution[] {
|
|
506
575
|
const daysBack = getDaysBack(period);
|
|
507
576
|
const byProject = simulator.generateHistoricalCostDataByProject(daysBack);
|
|
@@ -510,9 +579,11 @@ function fetchDemoProjectBreakdown(
|
|
|
510
579
|
const projectDayMap = new Map<string, Map<number, number>>();
|
|
511
580
|
|
|
512
581
|
for (const entry of byProject) {
|
|
513
|
-
const displayName = entry.projectPath.split(
|
|
582
|
+
const displayName = entry.projectPath.split("/").pop() ?? entry.projectPath;
|
|
514
583
|
if (!projectDayMap.has(displayName)) projectDayMap.set(displayName, new Map());
|
|
515
|
-
projectDayMap
|
|
584
|
+
projectDayMap
|
|
585
|
+
.get(displayName)!
|
|
586
|
+
.set(entry.date, (projectDayMap.get(displayName)!.get(entry.date) ?? 0) + entry.cost);
|
|
516
587
|
|
|
517
588
|
const existing = projectMap.get(displayName) ?? { cost: 0, tokens: 0, requests: 0 };
|
|
518
589
|
existing.cost += entry.cost;
|
|
@@ -522,12 +593,12 @@ function fetchDemoProjectBreakdown(
|
|
|
522
593
|
}
|
|
523
594
|
|
|
524
595
|
const totalCost = Array.from(projectMap.values()).reduce((sum, p) => sum + p.cost, 0);
|
|
525
|
-
const allDates = [...new Set(byProject.map(e => e.date))].sort((a, b) => a - b);
|
|
596
|
+
const allDates = [...new Set(byProject.map((e) => e.date))].sort((a, b) => a - b);
|
|
526
597
|
|
|
527
598
|
return Array.from(projectMap.entries())
|
|
528
599
|
.map(([project, d]) => {
|
|
529
600
|
const dayMap = projectDayMap.get(project) ?? new Map();
|
|
530
|
-
const dailyCosts = allDates.map(date => dayMap.get(date) ?? 0);
|
|
601
|
+
const dailyCosts = allDates.map((date) => dayMap.get(date) ?? 0);
|
|
531
602
|
return {
|
|
532
603
|
provider: project,
|
|
533
604
|
cost: d.cost,
|
|
@@ -542,8 +613,8 @@ function fetchDemoProjectBreakdown(
|
|
|
542
613
|
}
|
|
543
614
|
|
|
544
615
|
function fetchDemoPrevContributors(
|
|
545
|
-
simulator: NonNullable<ReturnType<typeof useDemoMode>[
|
|
546
|
-
period: TimePeriod
|
|
616
|
+
simulator: NonNullable<ReturnType<typeof useDemoMode>["simulator"]>,
|
|
617
|
+
period: TimePeriod,
|
|
547
618
|
): ProviderContribution[] {
|
|
548
619
|
const daysBack = getDaysBack(period);
|
|
549
620
|
const allByProvider = simulator.generateHistoricalCostDataByProvider(daysBack * 2);
|
|
@@ -572,7 +643,7 @@ function fetchDemoPrevContributors(
|
|
|
572
643
|
// AsciiChart component (enhanced with comparison + cursor)
|
|
573
644
|
// ============================================================================
|
|
574
645
|
|
|
575
|
-
type CellType =
|
|
646
|
+
type CellType = "empty" | "main" | "ghost" | "cursor";
|
|
576
647
|
|
|
577
648
|
interface AsciiChartProps {
|
|
578
649
|
data: ChartPoint[];
|
|
@@ -589,35 +660,53 @@ interface AsciiChartProps {
|
|
|
589
660
|
}
|
|
590
661
|
|
|
591
662
|
const AsciiChart = ({
|
|
592
|
-
data,
|
|
593
|
-
|
|
663
|
+
data,
|
|
664
|
+
comparisonData,
|
|
665
|
+
metric,
|
|
666
|
+
cursorIndex,
|
|
667
|
+
height,
|
|
668
|
+
width,
|
|
669
|
+
color,
|
|
670
|
+
ghostColor,
|
|
671
|
+
cursorColor,
|
|
672
|
+
labelColor,
|
|
673
|
+
gridColor,
|
|
594
674
|
}: AsciiChartProps) => {
|
|
595
675
|
const chartHeight = height - 2;
|
|
596
676
|
const yAxisWidth = 6;
|
|
597
677
|
const chartWidth = width - yAxisWidth;
|
|
598
678
|
|
|
599
679
|
if (data.length < 2 || chartHeight < 3 || chartWidth < 10) {
|
|
600
|
-
return
|
|
680
|
+
return (
|
|
681
|
+
<box>
|
|
682
|
+
<text fg={labelColor}>Chart too small</text>
|
|
683
|
+
</box>
|
|
684
|
+
);
|
|
601
685
|
}
|
|
602
686
|
|
|
603
|
-
const values = data.map(d => getMetricValue(d, metric));
|
|
604
|
-
const compValues = comparisonData?.map(d => getMetricValue(d, metric)) ?? [];
|
|
687
|
+
const values = data.map((d) => getMetricValue(d, metric));
|
|
688
|
+
const compValues = comparisonData?.map((d) => getMetricValue(d, metric)) ?? [];
|
|
605
689
|
const allValues = [...values, ...compValues];
|
|
606
|
-
const maxVal = Math.max(...allValues, metric ===
|
|
690
|
+
const maxVal = Math.max(...allValues, metric === "cost" ? 10 : 100) * 1.1;
|
|
607
691
|
const minVal = 0;
|
|
608
692
|
|
|
609
693
|
const normalize = (v: number) =>
|
|
610
|
-
Math.min(
|
|
694
|
+
Math.min(
|
|
695
|
+
chartHeight - 1,
|
|
696
|
+
Math.max(0, Math.floor(((v - minVal) / (maxVal - minVal)) * chartHeight)),
|
|
697
|
+
);
|
|
611
698
|
|
|
612
|
-
const gridChars: string[][] = Array.from({ length: chartHeight }, () =>
|
|
699
|
+
const gridChars: string[][] = Array.from({ length: chartHeight }, () =>
|
|
700
|
+
Array(chartWidth).fill(" "),
|
|
701
|
+
);
|
|
613
702
|
const gridTypes: CellType[][] = Array.from({ length: chartHeight }, () =>
|
|
614
|
-
Array<CellType>(chartWidth).fill(
|
|
703
|
+
Array<CellType>(chartWidth).fill("empty"),
|
|
615
704
|
);
|
|
616
705
|
|
|
617
706
|
const plotLine = (
|
|
618
707
|
vals: number[],
|
|
619
708
|
chars: { h: string; v: string; tl: string; tr: string; bl: string; br: string },
|
|
620
|
-
cellType: CellType
|
|
709
|
+
cellType: CellType,
|
|
621
710
|
) => {
|
|
622
711
|
if (vals.length < 2) return;
|
|
623
712
|
const normalized = vals.map(normalize);
|
|
@@ -639,23 +728,47 @@ const AsciiChart = ({
|
|
|
639
728
|
}
|
|
640
729
|
|
|
641
730
|
if (y2 > y1) {
|
|
642
|
-
if (xMid < chartWidth && gridChars[y1]) {
|
|
731
|
+
if (xMid < chartWidth && gridChars[y1]) {
|
|
732
|
+
gridChars[y1]![xMid] = chars.br;
|
|
733
|
+
gridTypes[y1]![xMid] = cellType;
|
|
734
|
+
}
|
|
643
735
|
for (let y = y1 + 1; y < y2; y++) {
|
|
644
|
-
if (xMid < chartWidth && gridChars[y]) {
|
|
736
|
+
if (xMid < chartWidth && gridChars[y]) {
|
|
737
|
+
gridChars[y]![xMid] = chars.v;
|
|
738
|
+
gridTypes[y]![xMid] = cellType;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (xMid < chartWidth && gridChars[y2]) {
|
|
742
|
+
gridChars[y2]![xMid] = chars.tl;
|
|
743
|
+
gridTypes[y2]![xMid] = cellType;
|
|
645
744
|
}
|
|
646
|
-
if (xMid < chartWidth && gridChars[y2]) { gridChars[y2]![xMid] = chars.tl; gridTypes[y2]![xMid] = cellType; }
|
|
647
745
|
} else if (y2 < y1) {
|
|
648
|
-
if (xMid < chartWidth && gridChars[y1]) {
|
|
746
|
+
if (xMid < chartWidth && gridChars[y1]) {
|
|
747
|
+
gridChars[y1]![xMid] = chars.tr;
|
|
748
|
+
gridTypes[y1]![xMid] = cellType;
|
|
749
|
+
}
|
|
649
750
|
for (let y = y1 - 1; y > y2; y--) {
|
|
650
|
-
if (xMid < chartWidth && gridChars[y]) {
|
|
751
|
+
if (xMid < chartWidth && gridChars[y]) {
|
|
752
|
+
gridChars[y]![xMid] = chars.v;
|
|
753
|
+
gridTypes[y]![xMid] = cellType;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (xMid < chartWidth && gridChars[y2]) {
|
|
757
|
+
gridChars[y2]![xMid] = chars.bl;
|
|
758
|
+
gridTypes[y2]![xMid] = cellType;
|
|
651
759
|
}
|
|
652
|
-
if (xMid < chartWidth && gridChars[y2]) { gridChars[y2]![xMid] = chars.bl; gridTypes[y2]![xMid] = cellType; }
|
|
653
760
|
} else {
|
|
654
|
-
if (xMid < chartWidth && gridChars[y1]) {
|
|
761
|
+
if (xMid < chartWidth && gridChars[y1]) {
|
|
762
|
+
gridChars[y1]![xMid] = chars.h;
|
|
763
|
+
gridTypes[y1]![xMid] = cellType;
|
|
764
|
+
}
|
|
655
765
|
}
|
|
656
766
|
|
|
657
767
|
for (let x = xMid + 1; x < xEnd; x++) {
|
|
658
|
-
if (x < chartWidth && gridChars[y2]) {
|
|
768
|
+
if (x < chartWidth && gridChars[y2]) {
|
|
769
|
+
gridChars[y2]![x] = chars.h;
|
|
770
|
+
gridTypes[y2]![x] = cellType;
|
|
771
|
+
}
|
|
659
772
|
}
|
|
660
773
|
|
|
661
774
|
currentX += stepsPerPoint;
|
|
@@ -671,9 +784,9 @@ const AsciiChart = ({
|
|
|
671
784
|
|
|
672
785
|
// Plot ghost first (comparison), then main on top
|
|
673
786
|
if (compValues.length >= 2) {
|
|
674
|
-
plotLine(compValues, GHOST_CHARS,
|
|
787
|
+
plotLine(compValues, GHOST_CHARS, "ghost");
|
|
675
788
|
}
|
|
676
|
-
plotLine(values, CHART_CHARS,
|
|
789
|
+
plotLine(values, CHART_CHARS, "main");
|
|
677
790
|
|
|
678
791
|
// Cursor overlay — final pass, draws on top of chart data
|
|
679
792
|
let cursorCol: number | null = null;
|
|
@@ -683,12 +796,12 @@ const AsciiChart = ({
|
|
|
683
796
|
if (cursorCol >= 0 && cursorCol < chartWidth) {
|
|
684
797
|
for (let y = 0; y < chartHeight; y++) {
|
|
685
798
|
const cellType = gridTypes[y]?.[cursorCol];
|
|
686
|
-
if (cellType ===
|
|
687
|
-
gridChars[y]![cursorCol] =
|
|
688
|
-
} else if (cellType ===
|
|
689
|
-
gridChars[y]![cursorCol] =
|
|
799
|
+
if (cellType === "empty") {
|
|
800
|
+
gridChars[y]![cursorCol] = "┆";
|
|
801
|
+
} else if (cellType === "main" || cellType === "ghost") {
|
|
802
|
+
gridChars[y]![cursorCol] = "╋";
|
|
690
803
|
}
|
|
691
|
-
gridTypes[y]![cursorCol] =
|
|
804
|
+
gridTypes[y]![cursorCol] = "cursor";
|
|
692
805
|
}
|
|
693
806
|
}
|
|
694
807
|
}
|
|
@@ -699,25 +812,35 @@ const AsciiChart = ({
|
|
|
699
812
|
const rowVal = minVal + (r / (chartHeight - 1)) * (maxVal - minVal);
|
|
700
813
|
const labelCount = Math.max(3, Math.min(7, Math.floor(chartHeight / 5) + 1));
|
|
701
814
|
const labelStep = (chartHeight - 1) / (labelCount - 1);
|
|
702
|
-
const showLabel = Array.from({ length: labelCount }, (_, i) =>
|
|
703
|
-
|
|
815
|
+
const showLabel = Array.from({ length: labelCount }, (_, i) =>
|
|
816
|
+
Math.round(i * labelStep),
|
|
817
|
+
).includes(r);
|
|
818
|
+
const label = showLabel ? fmtYAxisLabel(rowVal, metric) : " ";
|
|
704
819
|
const sep = r === 0 ? CHART_CHARS.cross : CHART_CHARS.t_l;
|
|
705
820
|
|
|
706
821
|
rows.push(
|
|
707
822
|
<box key={`row-${r}`} flexDirection="row" height={1}>
|
|
708
|
-
<text width={5} height={1} fg={labelColor}>
|
|
709
|
-
|
|
823
|
+
<text width={5} height={1} fg={labelColor}>
|
|
824
|
+
{label}
|
|
825
|
+
</text>
|
|
826
|
+
<text width={1} height={1} fg={gridColor}>
|
|
827
|
+
{sep}
|
|
828
|
+
</text>
|
|
710
829
|
<text flexGrow={1} height={1}>
|
|
711
830
|
{(gridChars[r] ?? []).map((char, cx) => {
|
|
712
|
-
const type = gridTypes[r]?.[cx] ??
|
|
831
|
+
const type = gridTypes[r]?.[cx] ?? "empty";
|
|
713
832
|
let fg: string | undefined;
|
|
714
|
-
if (type ===
|
|
715
|
-
else if (type ===
|
|
716
|
-
else if (type ===
|
|
717
|
-
return
|
|
833
|
+
if (type === "main") fg = color;
|
|
834
|
+
else if (type === "ghost") fg = ghostColor;
|
|
835
|
+
else if (type === "cursor") fg = cursorColor;
|
|
836
|
+
return (
|
|
837
|
+
<span key={cx} {...(fg ? { fg } : {})}>
|
|
838
|
+
{char}
|
|
839
|
+
</span>
|
|
840
|
+
);
|
|
718
841
|
})}
|
|
719
842
|
</text>
|
|
720
|
-
</box
|
|
843
|
+
</box>,
|
|
721
844
|
);
|
|
722
845
|
}
|
|
723
846
|
|
|
@@ -730,22 +853,31 @@ const AsciiChart = ({
|
|
|
730
853
|
const padLen = Math.max(0, cursorCol + yAxisWidth - 1);
|
|
731
854
|
cursorIndicator = (
|
|
732
855
|
<box height={1}>
|
|
733
|
-
<text height={1} fg={cursorColor}>
|
|
856
|
+
<text height={1} fg={cursorColor}>
|
|
857
|
+
{" ".repeat(padLen)}
|
|
858
|
+
{indicatorText}
|
|
859
|
+
</text>
|
|
734
860
|
</box>
|
|
735
861
|
);
|
|
736
862
|
}
|
|
737
863
|
}
|
|
738
864
|
|
|
739
865
|
// X-axis labels
|
|
740
|
-
const labelParts = data
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
866
|
+
const labelParts = data
|
|
867
|
+
.map((d, i) => {
|
|
868
|
+
if (!d.label) return "";
|
|
869
|
+
if (cursorIndex === i) return `[${d.label}]`;
|
|
870
|
+
return d.label;
|
|
871
|
+
})
|
|
872
|
+
.filter((l) => l)
|
|
873
|
+
.join(" ")
|
|
874
|
+
.slice(0, chartWidth);
|
|
745
875
|
|
|
746
876
|
const labelRow = (
|
|
747
877
|
<box flexDirection="row" height={1} paddingLeft={yAxisWidth}>
|
|
748
|
-
<text height={1} fg={labelColor}>
|
|
878
|
+
<text height={1} fg={labelColor}>
|
|
879
|
+
{labelParts}
|
|
880
|
+
</text>
|
|
749
881
|
</box>
|
|
750
882
|
);
|
|
751
883
|
|
|
@@ -767,34 +899,69 @@ function PeriodSummarySection({ summary, metric }: { summary: PeriodSummary; met
|
|
|
767
899
|
|
|
768
900
|
return (
|
|
769
901
|
<box flexDirection="column" paddingX={1}>
|
|
770
|
-
<text fg={colors.textMuted} height={1}
|
|
902
|
+
<text fg={colors.textMuted} height={1}>
|
|
903
|
+
<strong>PERIOD SUMMARY</strong>
|
|
904
|
+
</text>
|
|
771
905
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
772
|
-
<text fg={colors.textMuted} height={1}>
|
|
773
|
-
|
|
906
|
+
<text fg={colors.textMuted} height={1}>
|
|
907
|
+
Total
|
|
908
|
+
</text>
|
|
909
|
+
<text fg={colors.success} height={1}>
|
|
910
|
+
<strong>
|
|
911
|
+
{fmtMetricValue(
|
|
912
|
+
metric === "cost"
|
|
913
|
+
? summary.totalCost
|
|
914
|
+
: metric === "tokens"
|
|
915
|
+
? summary.totalTokens
|
|
916
|
+
: summary.totalRequests,
|
|
917
|
+
metric,
|
|
918
|
+
)}
|
|
919
|
+
</strong>
|
|
920
|
+
</text>
|
|
774
921
|
</box>
|
|
775
922
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
776
|
-
<text fg={colors.textMuted} height={1}>
|
|
777
|
-
|
|
923
|
+
<text fg={colors.textMuted} height={1}>
|
|
924
|
+
Avg/day
|
|
925
|
+
</text>
|
|
926
|
+
<text fg={colors.text} height={1}>
|
|
927
|
+
{fmtMetricValue(summary.avgPerDay, metric)}
|
|
928
|
+
</text>
|
|
778
929
|
</box>
|
|
779
930
|
{summary.peakDay && (
|
|
780
931
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
781
|
-
<text fg={colors.textMuted} height={1}>
|
|
782
|
-
|
|
932
|
+
<text fg={colors.textMuted} height={1}>
|
|
933
|
+
Peak
|
|
934
|
+
</text>
|
|
935
|
+
<text fg={colors.warning} height={1}>
|
|
936
|
+
{summary.peakDay.label} {fmtMetricValue(summary.peakDay.value, metric)}
|
|
937
|
+
</text>
|
|
783
938
|
</box>
|
|
784
939
|
)}
|
|
785
940
|
{summary.lowDay && (
|
|
786
941
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
787
|
-
<text fg={colors.textMuted} height={1}>
|
|
788
|
-
|
|
942
|
+
<text fg={colors.textMuted} height={1}>
|
|
943
|
+
Low
|
|
944
|
+
</text>
|
|
945
|
+
<text fg={colors.info} height={1}>
|
|
946
|
+
{summary.lowDay.label} {fmtMetricValue(summary.lowDay.value, metric)}
|
|
947
|
+
</text>
|
|
789
948
|
</box>
|
|
790
949
|
)}
|
|
791
950
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
792
|
-
<text fg={colors.textMuted} height={1}>
|
|
793
|
-
|
|
951
|
+
<text fg={colors.textMuted} height={1}>
|
|
952
|
+
Requests
|
|
953
|
+
</text>
|
|
954
|
+
<text fg={colors.text} height={1}>
|
|
955
|
+
{fmtNumber(summary.totalRequests)}
|
|
956
|
+
</text>
|
|
794
957
|
</box>
|
|
795
958
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
796
|
-
<text fg={colors.textMuted} height={1}>
|
|
797
|
-
|
|
959
|
+
<text fg={colors.textMuted} height={1}>
|
|
960
|
+
Tokens
|
|
961
|
+
</text>
|
|
962
|
+
<text fg={colors.text} height={1}>
|
|
963
|
+
{fmtTokens(summary.totalTokens)}
|
|
964
|
+
</text>
|
|
798
965
|
</box>
|
|
799
966
|
</box>
|
|
800
967
|
);
|
|
@@ -804,7 +971,15 @@ function PeriodSummarySection({ summary, metric }: { summary: PeriodSummary; met
|
|
|
804
971
|
// InsightPanel — vs Previous Period
|
|
805
972
|
// ============================================================================
|
|
806
973
|
|
|
807
|
-
function DeltaSection({
|
|
974
|
+
function DeltaSection({
|
|
975
|
+
current,
|
|
976
|
+
previous,
|
|
977
|
+
hasPrevData,
|
|
978
|
+
}: {
|
|
979
|
+
current: PeriodSummary;
|
|
980
|
+
previous: PeriodSummary;
|
|
981
|
+
hasPrevData: boolean;
|
|
982
|
+
}) {
|
|
808
983
|
const colors = useColors();
|
|
809
984
|
const costDelta = fmtDeltaPct(current.totalCost, previous.totalCost, hasPrevData);
|
|
810
985
|
const tokensDelta = fmtDeltaPct(current.totalTokens, previous.totalTokens, hasPrevData);
|
|
@@ -812,18 +987,32 @@ function DeltaSection({ current, previous, hasPrevData }: { current: PeriodSumma
|
|
|
812
987
|
|
|
813
988
|
return (
|
|
814
989
|
<box flexDirection="column" paddingX={1}>
|
|
815
|
-
<text fg={colors.textMuted} height={1}
|
|
990
|
+
<text fg={colors.textMuted} height={1}>
|
|
991
|
+
<strong>vs PREVIOUS</strong>
|
|
992
|
+
</text>
|
|
816
993
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
817
|
-
<text fg={colors.textMuted} height={1}>
|
|
818
|
-
|
|
994
|
+
<text fg={colors.textMuted} height={1}>
|
|
995
|
+
Cost
|
|
996
|
+
</text>
|
|
997
|
+
<text fg={costDelta.positive ? colors.success : colors.error} height={1}>
|
|
998
|
+
{costDelta.text}
|
|
999
|
+
</text>
|
|
819
1000
|
</box>
|
|
820
1001
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
821
|
-
<text fg={colors.textMuted} height={1}>
|
|
822
|
-
|
|
1002
|
+
<text fg={colors.textMuted} height={1}>
|
|
1003
|
+
Tokens
|
|
1004
|
+
</text>
|
|
1005
|
+
<text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>
|
|
1006
|
+
{tokensDelta.text}
|
|
1007
|
+
</text>
|
|
823
1008
|
</box>
|
|
824
1009
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
825
|
-
<text fg={colors.textMuted} height={1}>
|
|
826
|
-
|
|
1010
|
+
<text fg={colors.textMuted} height={1}>
|
|
1011
|
+
Requests
|
|
1012
|
+
</text>
|
|
1013
|
+
<text fg={reqDelta.positive ? colors.success : colors.error} height={1}>
|
|
1014
|
+
{reqDelta.text}
|
|
1015
|
+
</text>
|
|
827
1016
|
</box>
|
|
828
1017
|
</box>
|
|
829
1018
|
);
|
|
@@ -834,7 +1023,10 @@ function DeltaSection({ current, previous, hasPrevData }: { current: PeriodSumma
|
|
|
834
1023
|
// ============================================================================
|
|
835
1024
|
|
|
836
1025
|
function ComparisonTable({
|
|
837
|
-
current,
|
|
1026
|
+
current,
|
|
1027
|
+
previous,
|
|
1028
|
+
currentContributors,
|
|
1029
|
+
previousContributors,
|
|
838
1030
|
}: {
|
|
839
1031
|
current: PeriodSummary;
|
|
840
1032
|
previous: PeriodSummary;
|
|
@@ -847,7 +1039,7 @@ function ComparisonTable({
|
|
|
847
1039
|
|
|
848
1040
|
const providerDeltas: Array<{ name: string; delta: ReturnType<typeof fmtDeltaPct> }> = [];
|
|
849
1041
|
for (const curr of currentContributors) {
|
|
850
|
-
const prev = previousContributors.find(p => p.provider === curr.provider);
|
|
1042
|
+
const prev = previousContributors.find((p) => p.provider === curr.provider);
|
|
851
1043
|
providerDeltas.push({
|
|
852
1044
|
name: curr.provider,
|
|
853
1045
|
delta: fmtDeltaPct(curr.cost, prev?.cost ?? 0),
|
|
@@ -856,46 +1048,84 @@ function ComparisonTable({
|
|
|
856
1048
|
|
|
857
1049
|
return (
|
|
858
1050
|
<box flexDirection="column" paddingX={1}>
|
|
859
|
-
<text fg={colors.textMuted} height={1}
|
|
1051
|
+
<text fg={colors.textMuted} height={1}>
|
|
1052
|
+
<strong>COMPARISON</strong>
|
|
1053
|
+
</text>
|
|
860
1054
|
<box flexDirection="row" height={1}>
|
|
861
|
-
<text fg={colors.textMuted} width={10} height={1}>
|
|
862
|
-
|
|
863
|
-
|
|
1055
|
+
<text fg={colors.textMuted} width={10} height={1}>
|
|
1056
|
+
{padRight("", 10)}
|
|
1057
|
+
</text>
|
|
1058
|
+
<text fg={colors.textSubtle} width={8} height={1}>
|
|
1059
|
+
{"This".padStart(8)}
|
|
1060
|
+
</text>
|
|
1061
|
+
<text fg={colors.textSubtle} width={8} height={1}>
|
|
1062
|
+
{"Prev".padStart(8)}
|
|
1063
|
+
</text>
|
|
864
1064
|
</box>
|
|
865
1065
|
<box flexDirection="row" height={1}>
|
|
866
|
-
<text fg={colors.textMuted} width={10} height={1}>
|
|
867
|
-
|
|
868
|
-
|
|
1066
|
+
<text fg={colors.textMuted} width={10} height={1}>
|
|
1067
|
+
{padRight("Cost", 10)}
|
|
1068
|
+
</text>
|
|
1069
|
+
<text fg={colors.text} width={8} height={1}>
|
|
1070
|
+
{fmtCurrency(current.totalCost).padStart(8)}
|
|
1071
|
+
</text>
|
|
1072
|
+
<text fg={colors.textSubtle} width={8} height={1}>
|
|
1073
|
+
{fmtCurrency(previous.totalCost).padStart(8)}
|
|
1074
|
+
</text>
|
|
869
1075
|
</box>
|
|
870
1076
|
<box flexDirection="row" height={1}>
|
|
871
|
-
<text fg={colors.textMuted} width={10} height={1}>
|
|
872
|
-
|
|
873
|
-
|
|
1077
|
+
<text fg={colors.textMuted} width={10} height={1}>
|
|
1078
|
+
{padRight("Tokens", 10)}
|
|
1079
|
+
</text>
|
|
1080
|
+
<text fg={colors.text} width={8} height={1}>
|
|
1081
|
+
{fmtTokens(current.totalTokens).padStart(8)}
|
|
1082
|
+
</text>
|
|
1083
|
+
<text fg={colors.textSubtle} width={8} height={1}>
|
|
1084
|
+
{fmtTokens(previous.totalTokens).padStart(8)}
|
|
1085
|
+
</text>
|
|
874
1086
|
</box>
|
|
875
1087
|
|
|
876
1088
|
<box paddingX={0} height={1}>
|
|
877
|
-
<text fg={colors.border} height={1}>
|
|
1089
|
+
<text fg={colors.border} height={1}>
|
|
1090
|
+
{"─".repeat(26)}
|
|
1091
|
+
</text>
|
|
878
1092
|
</box>
|
|
879
1093
|
|
|
880
1094
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
881
|
-
<text fg={colors.textMuted} height={1}>
|
|
882
|
-
|
|
1095
|
+
<text fg={colors.textMuted} height={1}>
|
|
1096
|
+
Cost Δ
|
|
1097
|
+
</text>
|
|
1098
|
+
<text fg={costDelta.positive ? colors.success : colors.error} height={1}>
|
|
1099
|
+
{costDelta.text}
|
|
1100
|
+
</text>
|
|
883
1101
|
</box>
|
|
884
1102
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
885
|
-
<text fg={colors.textMuted} height={1}>
|
|
886
|
-
|
|
1103
|
+
<text fg={colors.textMuted} height={1}>
|
|
1104
|
+
Tokens Δ
|
|
1105
|
+
</text>
|
|
1106
|
+
<text fg={tokensDelta.positive ? colors.success : colors.error} height={1}>
|
|
1107
|
+
{tokensDelta.text}
|
|
1108
|
+
</text>
|
|
887
1109
|
</box>
|
|
888
1110
|
|
|
889
1111
|
{providerDeltas.length > 0 && (
|
|
890
1112
|
<box flexDirection="column">
|
|
891
1113
|
<box paddingX={0} height={1}>
|
|
892
|
-
<text fg={colors.border} height={1}>
|
|
1114
|
+
<text fg={colors.border} height={1}>
|
|
1115
|
+
{"─".repeat(26)}
|
|
1116
|
+
</text>
|
|
893
1117
|
</box>
|
|
894
|
-
<text fg={colors.textMuted} height={1}
|
|
895
|
-
|
|
1118
|
+
<text fg={colors.textMuted} height={1}>
|
|
1119
|
+
<strong>BIGGEST CHANGES</strong>
|
|
1120
|
+
</text>
|
|
1121
|
+
{providerDeltas.slice(0, 3).map((pd) => (
|
|
896
1122
|
<box key={pd.name} flexDirection="row" justifyContent="space-between" height={1}>
|
|
897
|
-
<text fg={colors.text} height={1}>
|
|
898
|
-
|
|
1123
|
+
<text fg={colors.text} height={1}>
|
|
1124
|
+
{truncate(pd.name, 12)}
|
|
1125
|
+
</text>
|
|
1126
|
+
<text fg={pd.delta.positive ? colors.success : colors.error} height={1}>
|
|
1127
|
+
{pd.delta.text}
|
|
1128
|
+
</text>
|
|
899
1129
|
</box>
|
|
900
1130
|
))}
|
|
901
1131
|
</box>
|
|
@@ -909,7 +1139,9 @@ function ComparisonTable({
|
|
|
909
1139
|
// ============================================================================
|
|
910
1140
|
|
|
911
1141
|
function ContributorsSection({
|
|
912
|
-
contributors,
|
|
1142
|
+
contributors,
|
|
1143
|
+
breakdown,
|
|
1144
|
+
panelWidth,
|
|
913
1145
|
}: {
|
|
914
1146
|
contributors: ProviderContribution[];
|
|
915
1147
|
breakdown: BreakdownDimension;
|
|
@@ -917,7 +1149,7 @@ function ContributorsSection({
|
|
|
917
1149
|
}) {
|
|
918
1150
|
const colors = useColors();
|
|
919
1151
|
|
|
920
|
-
if (breakdown ===
|
|
1152
|
+
if (breakdown === "off" || contributors.length === 0) return null;
|
|
921
1153
|
|
|
922
1154
|
// panelWidth includes border(2) + paddingX(2) from InsightPanel = 4 cols overhead
|
|
923
1155
|
const innerWidth = panelWidth - 4;
|
|
@@ -927,14 +1159,21 @@ function ContributorsSection({
|
|
|
927
1159
|
|
|
928
1160
|
return (
|
|
929
1161
|
<box flexDirection="column" paddingX={1}>
|
|
930
|
-
<text fg={colors.textMuted} height={1}
|
|
1162
|
+
<text fg={colors.textMuted} height={1}>
|
|
1163
|
+
<strong>
|
|
1164
|
+
TOP{" "}
|
|
1165
|
+
{breakdown === "model" ? "MODELS" : breakdown === "project" ? "PROJECTS" : "PROVIDERS"}
|
|
1166
|
+
</strong>
|
|
1167
|
+
</text>
|
|
931
1168
|
{contributors.map((c) => (
|
|
932
1169
|
<box key={c.provider} flexDirection="column">
|
|
933
1170
|
<box flexDirection="row" height={1}>
|
|
934
1171
|
<text fg={colors.primary} width={nameWidth} height={1}>
|
|
935
1172
|
{padRight(truncate(c.provider, nameWidth), nameWidth)}
|
|
936
1173
|
</text>
|
|
937
|
-
<text fg={colors.text} height={1}>
|
|
1174
|
+
<text fg={colors.text} height={1}>
|
|
1175
|
+
{fmtCurrency(c.cost).padStart(costColWidth)}
|
|
1176
|
+
</text>
|
|
938
1177
|
</box>
|
|
939
1178
|
<box flexDirection="row" height={1}>
|
|
940
1179
|
<text fg={colors.textSubtle} width={5} height={1}>
|
|
@@ -955,7 +1194,12 @@ function ContributorsSection({
|
|
|
955
1194
|
// ============================================================================
|
|
956
1195
|
|
|
957
1196
|
function CursorDayDetail({
|
|
958
|
-
currentDay,
|
|
1197
|
+
currentDay,
|
|
1198
|
+
previousDay,
|
|
1199
|
+
metric,
|
|
1200
|
+
contributors,
|
|
1201
|
+
cursorIndex,
|
|
1202
|
+
breakdown,
|
|
959
1203
|
}: {
|
|
960
1204
|
currentDay: ChartPoint;
|
|
961
1205
|
previousDay: ChartPoint | null;
|
|
@@ -972,38 +1216,70 @@ function CursorDayDetail({
|
|
|
972
1216
|
|
|
973
1217
|
return (
|
|
974
1218
|
<box flexDirection="column" paddingX={1}>
|
|
975
|
-
<text fg={colors.primary} height={1}
|
|
1219
|
+
<text fg={colors.primary} height={1}>
|
|
1220
|
+
<strong>{currentDay.dayName} DETAIL</strong>
|
|
1221
|
+
</text>
|
|
976
1222
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
977
|
-
<text fg={colors.textMuted} height={1}>
|
|
978
|
-
|
|
1223
|
+
<text fg={colors.textMuted} height={1}>
|
|
1224
|
+
Cost
|
|
1225
|
+
</text>
|
|
1226
|
+
<text fg={colors.success} height={1}>
|
|
1227
|
+
<strong>{fmtCurrency(currentDay.costUsd)}</strong>
|
|
1228
|
+
</text>
|
|
979
1229
|
</box>
|
|
980
1230
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
981
|
-
<text fg={colors.textMuted} height={1}>
|
|
982
|
-
|
|
1231
|
+
<text fg={colors.textMuted} height={1}>
|
|
1232
|
+
Tokens
|
|
1233
|
+
</text>
|
|
1234
|
+
<text fg={colors.text} height={1}>
|
|
1235
|
+
{fmtTokens(currentDay.tokens)}
|
|
1236
|
+
</text>
|
|
983
1237
|
</box>
|
|
984
1238
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
985
|
-
<text fg={colors.textMuted} height={1}>
|
|
986
|
-
|
|
1239
|
+
<text fg={colors.textMuted} height={1}>
|
|
1240
|
+
Requests
|
|
1241
|
+
</text>
|
|
1242
|
+
<text fg={colors.text} height={1}>
|
|
1243
|
+
{fmtNumber(currentDay.requestCount)}
|
|
1244
|
+
</text>
|
|
987
1245
|
</box>
|
|
988
1246
|
{dayDelta && (
|
|
989
1247
|
<box flexDirection="row" justifyContent="space-between" height={1}>
|
|
990
|
-
<text fg={colors.textMuted} height={1}>
|
|
991
|
-
|
|
1248
|
+
<text fg={colors.textMuted} height={1}>
|
|
1249
|
+
vs prev day
|
|
1250
|
+
</text>
|
|
1251
|
+
<text fg={dayDelta.positive ? colors.success : colors.error} height={1}>
|
|
1252
|
+
{dayDelta.text}
|
|
1253
|
+
</text>
|
|
992
1254
|
</box>
|
|
993
1255
|
)}
|
|
994
1256
|
|
|
995
1257
|
{contributors.length > 0 && (
|
|
996
1258
|
<box flexDirection="column">
|
|
997
1259
|
<box height={1}>
|
|
998
|
-
<text fg={colors.border} height={1}>
|
|
1260
|
+
<text fg={colors.border} height={1}>
|
|
1261
|
+
{"─".repeat(20)}
|
|
1262
|
+
</text>
|
|
999
1263
|
</box>
|
|
1000
|
-
<text fg={colors.textMuted} height={1}
|
|
1264
|
+
<text fg={colors.textMuted} height={1}>
|
|
1265
|
+
<strong>
|
|
1266
|
+
{breakdown === "model"
|
|
1267
|
+
? "TOP MODELS"
|
|
1268
|
+
: breakdown === "project"
|
|
1269
|
+
? "TOP PROJECTS"
|
|
1270
|
+
: "PROVIDERS"}
|
|
1271
|
+
</strong>
|
|
1272
|
+
</text>
|
|
1001
1273
|
{contributors.map((c) => {
|
|
1002
1274
|
const dayCost = c.dailyCosts[cursorIndex] ?? 0;
|
|
1003
1275
|
return (
|
|
1004
1276
|
<box key={c.provider} flexDirection="row" justifyContent="space-between" height={1}>
|
|
1005
|
-
<text fg={colors.primary} height={1}>
|
|
1006
|
-
|
|
1277
|
+
<text fg={colors.primary} height={1}>
|
|
1278
|
+
{truncate(c.provider, 12)}
|
|
1279
|
+
</text>
|
|
1280
|
+
<text fg={colors.text} height={1}>
|
|
1281
|
+
{fmtCurrency(dayCost)}
|
|
1282
|
+
</text>
|
|
1007
1283
|
</box>
|
|
1008
1284
|
);
|
|
1009
1285
|
})}
|
|
@@ -1018,7 +1294,9 @@ function CursorDayDetail({
|
|
|
1018
1294
|
// ============================================================================
|
|
1019
1295
|
|
|
1020
1296
|
function CondensedStrip({
|
|
1021
|
-
summary,
|
|
1297
|
+
summary,
|
|
1298
|
+
prevSummary,
|
|
1299
|
+
contributors,
|
|
1022
1300
|
}: {
|
|
1023
1301
|
summary: PeriodSummary;
|
|
1024
1302
|
prevSummary: PeriodSummary;
|
|
@@ -1030,21 +1308,30 @@ function CondensedStrip({
|
|
|
1030
1308
|
return (
|
|
1031
1309
|
<box flexDirection="row" height={2} paddingX={1} gap={2}>
|
|
1032
1310
|
<box flexDirection="column">
|
|
1033
|
-
<text fg={colors.textMuted} height={1}>Total: <span fg={colors.success}><strong>{fmtCurrency(summary.totalCost)}</strong></span></text>
|
|
1034
1311
|
<text fg={colors.textMuted} height={1}>
|
|
1035
|
-
{
|
|
1312
|
+
Total:{" "}
|
|
1313
|
+
<span fg={colors.success}>
|
|
1314
|
+
<strong>{fmtCurrency(summary.totalCost)}</strong>
|
|
1315
|
+
</span>
|
|
1316
|
+
</text>
|
|
1317
|
+
<text fg={colors.textMuted} height={1}>
|
|
1318
|
+
{"Avg: "}
|
|
1319
|
+
{fmtCurrency(summary.avgPerDay)}
|
|
1320
|
+
{"/d "}
|
|
1036
1321
|
<span fg={costDelta.positive ? colors.success : colors.error}>{costDelta.text}</span>
|
|
1037
1322
|
</text>
|
|
1038
1323
|
</box>
|
|
1039
1324
|
{contributors.length > 0 && (
|
|
1040
1325
|
<box flexDirection="column">
|
|
1041
|
-
<text fg={colors.textMuted} height={1}>
|
|
1326
|
+
<text fg={colors.textMuted} height={1}>
|
|
1327
|
+
Top:
|
|
1328
|
+
</text>
|
|
1042
1329
|
<text height={1}>
|
|
1043
1330
|
{contributors.slice(0, 3).map((c, i) => (
|
|
1044
1331
|
<span key={c.provider}>
|
|
1045
|
-
{i > 0 ?
|
|
1332
|
+
{i > 0 ? " " : ""}
|
|
1046
1333
|
<span fg={colors.primary}>{truncate(c.provider, 8)}</span>
|
|
1047
|
-
<span fg={colors.text}>
|
|
1334
|
+
<span fg={colors.text}> {fmtCurrency(c.cost)}</span>
|
|
1048
1335
|
</span>
|
|
1049
1336
|
))}
|
|
1050
1337
|
</text>
|
|
@@ -1059,8 +1346,18 @@ function CondensedStrip({
|
|
|
1059
1346
|
// ============================================================================
|
|
1060
1347
|
|
|
1061
1348
|
function InsightPanel({
|
|
1062
|
-
summary,
|
|
1063
|
-
|
|
1349
|
+
summary,
|
|
1350
|
+
prevSummary,
|
|
1351
|
+
contributors,
|
|
1352
|
+
previousContributors,
|
|
1353
|
+
metric,
|
|
1354
|
+
breakdown,
|
|
1355
|
+
comparisonMode,
|
|
1356
|
+
cursorMode,
|
|
1357
|
+
cursorIndex,
|
|
1358
|
+
data,
|
|
1359
|
+
panelWidth,
|
|
1360
|
+
hasPrevData,
|
|
1064
1361
|
}: {
|
|
1065
1362
|
summary: PeriodSummary;
|
|
1066
1363
|
prevSummary: PeriodSummary;
|
|
@@ -1140,13 +1437,17 @@ function InsightPanel({
|
|
|
1140
1437
|
<PeriodSummarySection summary={summary} metric={metric} />
|
|
1141
1438
|
|
|
1142
1439
|
<box paddingX={1} height={1}>
|
|
1143
|
-
<text fg={colors.border} height={1}>
|
|
1440
|
+
<text fg={colors.border} height={1}>
|
|
1441
|
+
{"─".repeat(Math.max(0, panelWidth - 4))}
|
|
1442
|
+
</text>
|
|
1144
1443
|
</box>
|
|
1145
1444
|
|
|
1146
1445
|
<DeltaSection current={summary} previous={prevSummary} hasPrevData={hasPrevData} />
|
|
1147
1446
|
|
|
1148
1447
|
<box paddingX={1} height={1}>
|
|
1149
|
-
<text fg={colors.border} height={1}>
|
|
1448
|
+
<text fg={colors.border} height={1}>
|
|
1449
|
+
{"─".repeat(Math.max(0, panelWidth - 4))}
|
|
1450
|
+
</text>
|
|
1150
1451
|
</box>
|
|
1151
1452
|
|
|
1152
1453
|
<ContributorsSection
|
|
@@ -1169,9 +1470,9 @@ export function HistoricalTrendsView() {
|
|
|
1169
1470
|
const { width: terminalWidth, height: terminalHeight } = useTerminalDimensions();
|
|
1170
1471
|
|
|
1171
1472
|
// State
|
|
1172
|
-
const [period, setPeriod] = useState<TimePeriod>(
|
|
1173
|
-
const [metric, setMetric] = useState<MetricType>(
|
|
1174
|
-
const [breakdown, setBreakdown] = useState<BreakdownDimension>(
|
|
1473
|
+
const [period, setPeriod] = useState<TimePeriod>("7d");
|
|
1474
|
+
const [metric, setMetric] = useState<MetricType>("cost");
|
|
1475
|
+
const [breakdown, setBreakdown] = useState<BreakdownDimension>("provider");
|
|
1175
1476
|
const [showInsight, setShowInsight] = useState(true);
|
|
1176
1477
|
const [comparisonMode, setComparisonMode] = useState(false);
|
|
1177
1478
|
const [cursorMode, setCursorMode] = useState(false);
|
|
@@ -1198,7 +1499,10 @@ export function HistoricalTrendsView() {
|
|
|
1198
1499
|
const viewChrome = 8;
|
|
1199
1500
|
// Narrow mode has condensed strip (2 rows) + vertical gap (1 row)
|
|
1200
1501
|
const stripOverhead = isNarrow ? 3 : 0;
|
|
1201
|
-
const chartHeight = Math.max(
|
|
1502
|
+
const chartHeight = Math.max(
|
|
1503
|
+
isNarrow ? 6 : 8,
|
|
1504
|
+
terminalHeight - appOverhead - viewChrome - stripOverhead,
|
|
1505
|
+
);
|
|
1202
1506
|
|
|
1203
1507
|
// Fetch time series data
|
|
1204
1508
|
useEffect(() => {
|
|
@@ -1214,17 +1518,18 @@ export function HistoricalTrendsView() {
|
|
|
1214
1518
|
|
|
1215
1519
|
// Fetch breakdown contributors based on dimension
|
|
1216
1520
|
useEffect(() => {
|
|
1217
|
-
if (breakdown ===
|
|
1521
|
+
if (breakdown === "off") {
|
|
1218
1522
|
setContributors([]);
|
|
1219
1523
|
return;
|
|
1220
1524
|
}
|
|
1221
1525
|
if (demoMode && simulator) {
|
|
1222
|
-
if (breakdown ===
|
|
1223
|
-
else if (breakdown ===
|
|
1526
|
+
if (breakdown === "model") setContributors(fetchDemoModelBreakdown(simulator, period));
|
|
1527
|
+
else if (breakdown === "project")
|
|
1528
|
+
setContributors(fetchDemoProjectBreakdown(simulator, period));
|
|
1224
1529
|
else setContributors(fetchDemoProviderBreakdown(simulator, period));
|
|
1225
1530
|
} else if (isStorageReady) {
|
|
1226
|
-
if (breakdown ===
|
|
1227
|
-
else if (breakdown ===
|
|
1531
|
+
if (breakdown === "model") setContributors(fetchModelBreakdown(period));
|
|
1532
|
+
else if (breakdown === "project") setContributors(fetchProjectBreakdown(period));
|
|
1228
1533
|
else setContributors(fetchProviderBreakdown(period));
|
|
1229
1534
|
}
|
|
1230
1535
|
}, [isStorageReady, period, breakdown, demoMode, simulator]);
|
|
@@ -1254,7 +1559,7 @@ export function HistoricalTrendsView() {
|
|
|
1254
1559
|
requests: 0,
|
|
1255
1560
|
dailyCosts: [],
|
|
1256
1561
|
}))
|
|
1257
|
-
.sort((a, b) => b.cost - a.cost)
|
|
1562
|
+
.sort((a, b) => b.cost - a.cost),
|
|
1258
1563
|
);
|
|
1259
1564
|
} catch {
|
|
1260
1565
|
setPrevContributors([]);
|
|
@@ -1269,86 +1574,110 @@ export function HistoricalTrendsView() {
|
|
|
1269
1574
|
// Computed values
|
|
1270
1575
|
const summary = useMemo(() => computeSummary(data, metric), [data, metric]);
|
|
1271
1576
|
const prevSummary = useMemo(() => computeSummary(prevData, metric), [prevData, metric]);
|
|
1272
|
-
const hasData = data.some(p => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
|
|
1273
|
-
const hasPrevData = prevData.some(p => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
|
|
1577
|
+
const hasData = data.some((p) => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
|
|
1578
|
+
const hasPrevData = prevData.some((p) => p.costUsd > 0 || p.tokens > 0 || p.requestCount > 0);
|
|
1274
1579
|
const totalForMetric = useMemo(() => {
|
|
1275
1580
|
switch (metric) {
|
|
1276
|
-
case
|
|
1277
|
-
|
|
1278
|
-
case
|
|
1581
|
+
case "cost":
|
|
1582
|
+
return data.reduce((acc, p) => acc + p.costUsd, 0);
|
|
1583
|
+
case "tokens":
|
|
1584
|
+
return data.reduce((acc, p) => acc + p.tokens, 0);
|
|
1585
|
+
case "requests":
|
|
1586
|
+
return data.reduce((acc, p) => acc + p.requestCount, 0);
|
|
1279
1587
|
}
|
|
1280
1588
|
}, [data, metric]);
|
|
1281
1589
|
|
|
1282
1590
|
// Keyboard
|
|
1283
1591
|
useKeyboard((key) => {
|
|
1284
|
-
if (key.name ===
|
|
1592
|
+
if (key.name === "left" || key.name === "h") {
|
|
1285
1593
|
if (cursorMode) {
|
|
1286
|
-
setCursorIndex(prev => Math.max(0, prev - 1));
|
|
1594
|
+
setCursorIndex((prev) => Math.max(0, prev - 1));
|
|
1287
1595
|
} else {
|
|
1288
|
-
setPeriod(prev => prev ===
|
|
1596
|
+
setPeriod((prev) => (prev === "90d" ? "30d" : prev === "30d" ? "7d" : "7d"));
|
|
1289
1597
|
}
|
|
1290
1598
|
}
|
|
1291
|
-
if (key.name ===
|
|
1599
|
+
if (key.name === "right" || key.name === "l") {
|
|
1292
1600
|
if (cursorMode) {
|
|
1293
|
-
setCursorIndex(prev => Math.min(data.length - 1, prev + 1));
|
|
1601
|
+
setCursorIndex((prev) => Math.min(data.length - 1, prev + 1));
|
|
1294
1602
|
} else {
|
|
1295
|
-
setPeriod(prev => prev ===
|
|
1603
|
+
setPeriod((prev) => (prev === "7d" ? "30d" : prev === "30d" ? "90d" : "90d"));
|
|
1296
1604
|
}
|
|
1297
1605
|
}
|
|
1298
1606
|
|
|
1299
|
-
if (key.name ===
|
|
1607
|
+
if (key.name === "down" || key.name === "j") {
|
|
1300
1608
|
if (!cursorMode) {
|
|
1301
1609
|
setCursorMode(true);
|
|
1302
1610
|
setCursorIndex(0);
|
|
1303
1611
|
} else {
|
|
1304
|
-
setCursorIndex(prev => Math.min(data.length - 1, prev + 1));
|
|
1612
|
+
setCursorIndex((prev) => Math.min(data.length - 1, prev + 1));
|
|
1305
1613
|
}
|
|
1306
1614
|
}
|
|
1307
|
-
if (key.name ===
|
|
1615
|
+
if (key.name === "up" || key.name === "k") {
|
|
1308
1616
|
if (!cursorMode) {
|
|
1309
1617
|
setCursorMode(true);
|
|
1310
1618
|
setCursorIndex(0);
|
|
1311
1619
|
} else {
|
|
1312
|
-
setCursorIndex(prev => Math.max(0, prev - 1));
|
|
1620
|
+
setCursorIndex((prev) => Math.max(0, prev - 1));
|
|
1313
1621
|
}
|
|
1314
1622
|
}
|
|
1315
1623
|
|
|
1316
|
-
if (key.name ===
|
|
1624
|
+
if (key.name === "escape") {
|
|
1317
1625
|
if (cursorMode) setCursorMode(false);
|
|
1318
1626
|
else if (comparisonMode) setComparisonMode(false);
|
|
1319
1627
|
}
|
|
1320
1628
|
|
|
1321
|
-
if (key.name ===
|
|
1322
|
-
if (key.name ===
|
|
1323
|
-
|
|
1324
|
-
if (key.name ===
|
|
1629
|
+
if (key.name === "c") setComparisonMode((prev) => !prev);
|
|
1630
|
+
if (key.name === "m")
|
|
1631
|
+
setMetric((prev) => (prev === "cost" ? "tokens" : prev === "tokens" ? "requests" : "cost"));
|
|
1632
|
+
if (key.name === "b")
|
|
1633
|
+
setBreakdown((prev) =>
|
|
1634
|
+
prev === "provider"
|
|
1635
|
+
? "model"
|
|
1636
|
+
: prev === "model"
|
|
1637
|
+
? "project"
|
|
1638
|
+
: prev === "project"
|
|
1639
|
+
? "off"
|
|
1640
|
+
: "provider",
|
|
1641
|
+
);
|
|
1642
|
+
if (key.name === "i") setShowInsight((prev) => !prev);
|
|
1325
1643
|
});
|
|
1326
1644
|
|
|
1327
|
-
const periodLabel = period ===
|
|
1645
|
+
const periodLabel = period === "7d" ? "7 days" : period === "30d" ? "30 days" : "90 days";
|
|
1328
1646
|
const modeIndicators: string[] = [];
|
|
1329
|
-
if (comparisonMode) modeIndicators.push(
|
|
1330
|
-
if (cursorMode) modeIndicators.push(
|
|
1331
|
-
if (metric !==
|
|
1647
|
+
if (comparisonMode) modeIndicators.push("CMP");
|
|
1648
|
+
if (cursorMode) modeIndicators.push("CURSOR");
|
|
1649
|
+
if (metric !== "cost") modeIndicators.push(METRIC_LABELS[metric]);
|
|
1332
1650
|
|
|
1333
1651
|
return (
|
|
1334
|
-
<box
|
|
1652
|
+
<box
|
|
1653
|
+
flexDirection="column"
|
|
1654
|
+
flexGrow={1}
|
|
1655
|
+
padding={1}
|
|
1656
|
+
border
|
|
1657
|
+
borderStyle="single"
|
|
1658
|
+
borderColor={colors.border}
|
|
1659
|
+
>
|
|
1335
1660
|
{/* Header */}
|
|
1336
1661
|
<box flexDirection="row" justifyContent="space-between" height={1} marginBottom={1}>
|
|
1337
1662
|
<text height={1}>
|
|
1338
|
-
<span fg={colors.primary}
|
|
1663
|
+
<span fg={colors.primary}>
|
|
1664
|
+
<strong> {METRIC_LABELS[metric]} TREND </strong>
|
|
1665
|
+
</span>
|
|
1339
1666
|
<span fg={colors.textMuted}>({periodLabel})</span>
|
|
1340
1667
|
{modeIndicators.length > 0 && (
|
|
1341
|
-
<span fg={colors.accent}>
|
|
1668
|
+
<span fg={colors.accent}> [{modeIndicators.join(" | ")}]</span>
|
|
1342
1669
|
)}
|
|
1343
1670
|
</text>
|
|
1344
1671
|
<text height={1}>
|
|
1345
1672
|
<span fg={colors.textMuted}>Total: </span>
|
|
1346
|
-
<span fg={colors.success}
|
|
1673
|
+
<span fg={colors.success}>
|
|
1674
|
+
<strong>{fmtMetricValue(totalForMetric, metric)}</strong>
|
|
1675
|
+
</span>
|
|
1347
1676
|
</text>
|
|
1348
1677
|
</box>
|
|
1349
1678
|
|
|
1350
1679
|
{/* Main content */}
|
|
1351
|
-
<box flexGrow={1} flexDirection={isNarrow ?
|
|
1680
|
+
<box flexGrow={1} flexDirection={isNarrow ? "column" : "row"} gap={1}>
|
|
1352
1681
|
{/* Chart pane */}
|
|
1353
1682
|
<box flexGrow={1} flexDirection="column" justifyContent="center" overflow="hidden">
|
|
1354
1683
|
{!isStorageReady && !demoMode ? (
|
|
@@ -1376,8 +1705,9 @@ export function HistoricalTrendsView() {
|
|
|
1376
1705
|
</box>
|
|
1377
1706
|
|
|
1378
1707
|
{/* Insight panel */}
|
|
1379
|
-
{showInsight &&
|
|
1380
|
-
|
|
1708
|
+
{showInsight &&
|
|
1709
|
+
hasData &&
|
|
1710
|
+
(isNarrow ? (
|
|
1381
1711
|
<CondensedStrip
|
|
1382
1712
|
summary={summary}
|
|
1383
1713
|
prevSummary={prevSummary}
|
|
@@ -1398,8 +1728,7 @@ export function HistoricalTrendsView() {
|
|
|
1398
1728
|
panelWidth={panelWidth}
|
|
1399
1729
|
hasPrevData={hasPrevData}
|
|
1400
1730
|
/>
|
|
1401
|
-
)
|
|
1402
|
-
)}
|
|
1731
|
+
))}
|
|
1403
1732
|
</box>
|
|
1404
1733
|
|
|
1405
1734
|
{/* Footer */}
|
|
@@ -1409,19 +1738,19 @@ export function HistoricalTrendsView() {
|
|
|
1409
1738
|
<span>
|
|
1410
1739
|
<span fg={colors.accent}>CURSOR</span>
|
|
1411
1740
|
{isNarrow
|
|
1412
|
-
?
|
|
1413
|
-
:
|
|
1741
|
+
? " ←→ move Esc exit c cmp m met"
|
|
1742
|
+
: " ←→ move Esc exit c compare m metric i panel"}
|
|
1414
1743
|
</span>
|
|
1415
1744
|
) : (
|
|
1416
1745
|
<span>
|
|
1417
|
-
<span fg={period ===
|
|
1418
|
-
{
|
|
1419
|
-
<span fg={period ===
|
|
1420
|
-
{
|
|
1421
|
-
<span fg={period ===
|
|
1746
|
+
<span fg={period === "7d" ? colors.primary : colors.textSubtle}>7d</span>
|
|
1747
|
+
{" "}
|
|
1748
|
+
<span fg={period === "30d" ? colors.primary : colors.textSubtle}>30d</span>
|
|
1749
|
+
{" "}
|
|
1750
|
+
<span fg={period === "90d" ? colors.primary : colors.textSubtle}>90d</span>
|
|
1422
1751
|
{isNarrow
|
|
1423
|
-
?
|
|
1424
|
-
:
|
|
1752
|
+
? " h/l per j/k cur c cmp m met b brk i pan"
|
|
1753
|
+
: " h/l period ↑↓ cursor c compare m metric b breakdown i panel"}
|
|
1425
1754
|
</span>
|
|
1426
1755
|
)}
|
|
1427
1756
|
</text>
|