Skip to content

Commit 981069e

Browse files
committed
Add cost tracking page
Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 37ee558 commit 981069e

16 files changed

Lines changed: 1893 additions & 1397 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { useMemo } from 'react';
2+
import {
3+
LineChart,
4+
Line,
5+
XAxis,
6+
YAxis,
7+
CartesianGrid,
8+
Tooltip,
9+
Legend,
10+
ResponsiveContainer,
11+
} from 'recharts';
12+
import { format, subDays, startOfDay } from 'date-fns';
13+
import type { DailyCostUsage } from '../../hooks/use-cost-usage';
14+
15+
interface DailyCostUsageChartProps {
16+
data: DailyCostUsage[];
17+
timeRange: '7days' | '1month' | '3months';
18+
}
19+
20+
type TimeRange = '7days' | '1month' | '3months';
21+
22+
const TIME_RANGE_OPTIONS: { value: TimeRange; label: string; days: number }[] = [
23+
{ value: '7days', label: '7 Days', days: 7 },
24+
{ value: '1month', label: '1 Month', days: 30 },
25+
{ value: '3months', label: '3 Months', days: 90 },
26+
];
27+
28+
export function DailyCostUsageChart({ data, timeRange }: DailyCostUsageChartProps) {
29+
const chartData = useMemo(() => {
30+
const selectedRange = TIME_RANGE_OPTIONS.find((r) => r.value === timeRange);
31+
if (!selectedRange) return [];
32+
33+
const now = new Date();
34+
35+
// Create date map for aggregation - fill all dates with 0
36+
const dateMap = new Map<string, {
37+
totalCost: number;
38+
totalTokens: number;
39+
inputTokens: number;
40+
outputTokens: number;
41+
cacheReadInputTokens: number;
42+
cacheCreationInputTokens: number;
43+
}>();
44+
45+
// Initialize all dates in range with 0
46+
for (let i = 0; i < selectedRange.days; i++) {
47+
const date = subDays(now, selectedRange.days - 1 - i);
48+
const dateKey = format(startOfDay(date), 'yyyy-MM-dd');
49+
dateMap.set(dateKey, {
50+
totalCost: 0,
51+
totalTokens: 0,
52+
inputTokens: 0,
53+
outputTokens: 0,
54+
cacheReadInputTokens: 0,
55+
cacheCreationInputTokens: 0,
56+
});
57+
}
58+
59+
// Fill in actual data
60+
data.forEach((item) => {
61+
const dateKey = format(new Date(item.date), 'yyyy-MM-dd');
62+
if (dateMap.has(dateKey)) {
63+
dateMap.set(dateKey, {
64+
totalCost: item.totalCost,
65+
totalTokens: item.totalTokens,
66+
inputTokens: item.inputTokens,
67+
outputTokens: item.outputTokens,
68+
cacheReadInputTokens: item.cacheReadInputTokens,
69+
cacheCreationInputTokens: item.cacheCreationInputTokens,
70+
});
71+
}
72+
});
73+
74+
// Convert to array and format for chart
75+
return Array.from(dateMap.entries())
76+
.map(([date, values]) => ({
77+
date,
78+
displayDate: format(new Date(date), 'MMM dd'),
79+
totalCost: values.totalCost,
80+
totalTokens: values.totalTokens,
81+
inputTokens: values.inputTokens,
82+
outputTokens: values.outputTokens,
83+
cacheReadInputTokens: values.cacheReadInputTokens,
84+
cacheCreationInputTokens: values.cacheCreationInputTokens,
85+
}))
86+
.sort((a, b) => a.date.localeCompare(b.date));
87+
}, [data, timeRange]);
88+
89+
// Calculate totals across all days
90+
const totalCost = useMemo(() => {
91+
return chartData.reduce((sum, item) => sum + item.totalCost, 0);
92+
}, [chartData]);
93+
94+
const totalTokens = useMemo(() => {
95+
return chartData.reduce((sum, item) => sum + item.totalTokens, 0);
96+
}, [chartData]);
97+
98+
const totalInputTokens = useMemo(() => {
99+
return chartData.reduce((sum, item) => sum + item.inputTokens, 0);
100+
}, [chartData]);
101+
102+
const totalOutputTokens = useMemo(() => {
103+
return chartData.reduce((sum, item) => sum + item.outputTokens, 0);
104+
}, [chartData]);
105+
106+
const totalCacheReadInputTokens = useMemo(() => {
107+
return chartData.reduce((sum, item) => sum + item.cacheReadInputTokens, 0);
108+
}, [chartData]);
109+
110+
const totalCacheCreationInputTokens = useMemo(() => {
111+
return chartData.reduce((sum, item) => sum + item.cacheCreationInputTokens, 0);
112+
}, [chartData]);
113+
114+
return (
115+
<div className="space-y-1.5">
116+
<div className="flex items-center justify-between">
117+
<h3 className="text-xs font-semibold">Cost & Token Usage</h3>
118+
<div className="text-[10px] text-muted-foreground">
119+
${totalCost.toFixed(4)}{totalTokens.toLocaleString()} tokens (in:{totalInputTokens.toLocaleString()} out:{totalOutputTokens.toLocaleString()} cache-r:{totalCacheReadInputTokens.toLocaleString()} cache-c:{totalCacheCreationInputTokens.toLocaleString()})
120+
</div>
121+
</div>
122+
123+
<ResponsiveContainer width="100%" height={200}>
124+
<LineChart data={chartData} margin={{ left: 5, right: 45, top: 5, bottom: 0 }}>
125+
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" opacity={0.5} />
126+
<XAxis
127+
dataKey="displayDate"
128+
tick={{ fontSize: 9 }}
129+
angle={-45}
130+
textAnchor="end"
131+
height={40}
132+
/>
133+
{/* Left Y-Axis for Tokens */}
134+
<YAxis
135+
yAxisId="tokens"
136+
tick={{ fontSize: 9 }}
137+
width={40}
138+
tickFormatter={(value) => {
139+
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
140+
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
141+
return value.toString();
142+
}}
143+
label={{
144+
value: 'Tokens',
145+
angle: -90,
146+
position: 'insideLeft',
147+
offset: 0,
148+
style: { textAnchor: 'middle', fontSize: 9 }
149+
}}
150+
/>
151+
{/* Right Y-Axis for Cost */}
152+
<YAxis
153+
yAxisId="cost"
154+
orientation="right"
155+
tick={{ fontSize: 9 }}
156+
width={45}
157+
tickFormatter={(value) =>
158+
value >= 1
159+
? `$${value.toFixed(2)}`
160+
: value >= 0.01
161+
? `$${value.toFixed(3)}`
162+
: `$${value.toFixed(4)}`
163+
}
164+
label={{
165+
value: 'Cost (USD)',
166+
angle: 90,
167+
position: 'insideRight',
168+
offset: 0,
169+
style: { textAnchor: 'middle', fontSize: 9 }
170+
}}
171+
/>
172+
<Tooltip
173+
contentStyle={{
174+
backgroundColor: 'hsl(var(--card))',
175+
border: '1px solid hsl(var(--border))',
176+
borderRadius: '6px',
177+
fontSize: '10px',
178+
}}
179+
content={({ active, payload, label }) => {
180+
if (!active || !payload || !payload.length) return null;
181+
const data = payload[0].payload;
182+
return (
183+
<div className="bg-card border border-border rounded-md p-2 shadow-sm">
184+
<div className="text-[10px] font-medium mb-1.5">{label}</div>
185+
<div className="space-y-0.5 text-[10px]">
186+
<div className="flex items-center gap-2">
187+
<div className="w-2 h-2 rounded-full bg-emerald-500"></div>
188+
<span className="text-muted-foreground">Cost:</span>
189+
<span className="font-medium ml-auto">${data.totalCost.toFixed(4)}</span>
190+
</div>
191+
<div className="flex items-center gap-2">
192+
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
193+
<span className="text-muted-foreground">Total Tokens:</span>
194+
<span className="font-medium ml-auto">{data.totalTokens.toLocaleString()}</span>
195+
</div>
196+
<div className="flex items-center gap-2">
197+
<div className="w-2 h-2 rounded-full bg-purple-500"></div>
198+
<span className="text-muted-foreground">Input:</span>
199+
<span className="font-medium ml-auto">{data.inputTokens.toLocaleString()}</span>
200+
</div>
201+
<div className="flex items-center gap-2">
202+
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
203+
<span className="text-muted-foreground">Output:</span>
204+
<span className="font-medium ml-auto">{data.outputTokens.toLocaleString()}</span>
205+
</div>
206+
<div className="flex items-center gap-2">
207+
<div className="w-2 h-2 rounded-full bg-cyan-500"></div>
208+
<span className="text-muted-foreground">Cache Read:</span>
209+
<span className="font-medium ml-auto">{data.cacheReadInputTokens.toLocaleString()}</span>
210+
</div>
211+
<div className="flex items-center gap-2">
212+
<div className="w-2 h-2 rounded-full bg-pink-500"></div>
213+
<span className="text-muted-foreground">Cache Creation:</span>
214+
<span className="font-medium ml-auto">{data.cacheCreationInputTokens.toLocaleString()}</span>
215+
</div>
216+
</div>
217+
</div>
218+
);
219+
}}
220+
/>
221+
<Legend
222+
wrapperStyle={{ fontSize: '10px', paddingTop: '2px' }}
223+
iconType="circle"
224+
iconSize={6}
225+
verticalAlign="bottom"
226+
height={20}
227+
/>
228+
{/* Token Lines - Left Y-Axis */}
229+
<Line
230+
yAxisId="tokens"
231+
type="monotone"
232+
dataKey="totalTokens"
233+
stroke="#3b82f6"
234+
strokeWidth={2}
235+
dot={{ fill: '#3b82f6', r: 3 }}
236+
activeDot={{ r: 5 }}
237+
name="Total Tokens"
238+
/>
239+
<Line
240+
yAxisId="tokens"
241+
type="monotone"
242+
dataKey="inputTokens"
243+
stroke="#a855f7"
244+
strokeWidth={2}
245+
dot={{ fill: '#a855f7', r: 3 }}
246+
activeDot={{ r: 5 }}
247+
name="Input"
248+
/>
249+
<Line
250+
yAxisId="tokens"
251+
type="monotone"
252+
dataKey="outputTokens"
253+
stroke="#f59e0b"
254+
strokeWidth={2}
255+
dot={{ fill: '#f59e0b', r: 3 }}
256+
activeDot={{ r: 5 }}
257+
name="Output"
258+
/>
259+
<Line
260+
yAxisId="tokens"
261+
type="monotone"
262+
dataKey="cacheReadInputTokens"
263+
stroke="#06b6d4"
264+
strokeWidth={2}
265+
dot={{ fill: '#06b6d4', r: 3 }}
266+
activeDot={{ r: 5 }}
267+
name="Cache Read"
268+
/>
269+
<Line
270+
yAxisId="tokens"
271+
type="monotone"
272+
dataKey="cacheCreationInputTokens"
273+
stroke="#ec4899"
274+
strokeWidth={2}
275+
dot={{ fill: '#ec4899', r: 3 }}
276+
activeDot={{ r: 5 }}
277+
name="Cache Creation"
278+
/>
279+
{/* Cost Line - Right Y-Axis */}
280+
<Line
281+
yAxisId="cost"
282+
type="monotone"
283+
dataKey="totalCost"
284+
stroke="#10b981"
285+
strokeWidth={2}
286+
dot={{ fill: '#10b981', r: 3 }}
287+
activeDot={{ r: 5 }}
288+
name="Cost"
289+
/>
290+
</LineChart>
291+
</ResponsiveContainer>
292+
</div>
293+
);
294+
}

0 commit comments

Comments
 (0)