@@ -5,6 +5,21 @@ import type { CostSummary, BudgetStatus, ProviderBreakdown, DailyUsage } from '.
55import { LoadingSpinner } from '../components/LoadingSpinner' ;
66import { useToast } from '../components/ToastProvider' ;
77import { DollarSign , Home , BarChart , TrendingUp , Calendar , Layers } from '../components/icons' ;
8+ import {
9+ AreaChart ,
10+ Area ,
11+ BarChart as RechartsBarChart ,
12+ Bar ,
13+ PieChart ,
14+ Pie ,
15+ Cell ,
16+ XAxis ,
17+ YAxis ,
18+ CartesianGrid ,
19+ Tooltip ,
20+ Legend ,
21+ ResponsiveContainer ,
22+ } from 'recharts' ;
823import { PageHomeTab } from '../components/PageHomeTab' ;
924
1025type Period = 'day' | 'week' | 'month' | 'year' ;
@@ -304,49 +319,227 @@ export function CostsPage() {
304319 </ div >
305320 ) }
306321
307- { /* Daily Chart */ }
322+ { /* Daily Cost Trend */ }
308323 { breakdown && breakdown . daily . length > 0 && (
309324 < div className = "card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4" >
310- < h3 className = "text-lg font-medium text-text-primary dark:text-dark-text-primary mb-4" >
311- Daily Usage
325+ < h3 className = "text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4" >
326+ < TrendingUp className = "w-4 h-4 text-success" />
327+ Daily Cost Trend
312328 </ h3 >
313- < div className = "flex gap-1 items-end h-32" >
314- { breakdown . daily . slice ( - 14 ) . map ( ( day ) => {
315- const maxCost = Math . max ( ...breakdown . daily . map ( ( d ) => d . cost ) ) ;
316- const height = maxCost > 0 ? ( day . cost / maxCost ) * 100 : 0 ;
317- return (
318- < div
319- key = { day . date }
320- className = "flex-1 group relative"
321- title = { `${ day . date } : ${ day . costFormatted } ` }
322- >
323- < div
324- className = "bg-success rounded-t transition-all hover:bg-success/80"
325- style = { { height : `${ Math . max ( height , 2 ) } %` } }
326- />
327- < div className = "absolute bottom-full left-1/2 -translate-x-1/2 mb-1 hidden group-hover:block bg-gray-900 text-white text-xs px-2 py-1 rounded whitespace-nowrap" >
328- { day . date } : { day . costFormatted }
329- </ div >
330- </ div >
331- ) ;
332- } ) }
333- </ div >
334- < div className = "flex justify-between mt-2 text-xs text-text-muted" >
335- < span > { breakdown . daily [ 0 ] ?. date } </ span >
336- < span > { breakdown . daily [ breakdown . daily . length - 1 ] ?. date } </ span >
337- </ div >
329+ < ResponsiveContainer width = "100%" height = { 220 } >
330+ < AreaChart
331+ data = { breakdown . daily . map ( ( d ) => ( {
332+ ...d ,
333+ date : new Date ( d . date ) . toLocaleDateString ( 'en' , {
334+ month : 'short' ,
335+ day : 'numeric' ,
336+ } ) ,
337+ } ) ) }
338+ >
339+ < defs >
340+ < linearGradient id = "costAreaGrad" x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
341+ < stop offset = "5%" stopColor = "#22c55e" stopOpacity = { 0.25 } />
342+ < stop offset = "95%" stopColor = "#22c55e" stopOpacity = { 0 } />
343+ </ linearGradient >
344+ </ defs >
345+ < CartesianGrid
346+ strokeDasharray = "3 3"
347+ stroke = "var(--color-border, #334155)"
348+ opacity = { 0.4 }
349+ />
350+ < XAxis
351+ dataKey = "date"
352+ tick = { { fontSize : 11 } }
353+ stroke = "var(--color-text-muted, #94a3b8)"
354+ />
355+ < YAxis
356+ tick = { { fontSize : 11 } }
357+ stroke = "var(--color-text-muted, #94a3b8)"
358+ tickFormatter = { ( v ) => `$${ v } ` }
359+ />
360+ < Tooltip
361+ contentStyle = { {
362+ background : 'var(--color-bg-secondary, #1e293b)' ,
363+ border : '1px solid var(--color-border, #334155)' ,
364+ borderRadius : '8px' ,
365+ fontSize : '12px' ,
366+ } }
367+ formatter = { ( value : unknown ) => [ `$${ Number ( value ) . toFixed ( 4 ) } ` , 'Cost' ] }
368+ />
369+ < Area
370+ type = "monotone"
371+ dataKey = "cost"
372+ stroke = "#22c55e"
373+ fill = "url(#costAreaGrad)"
374+ strokeWidth = { 2 }
375+ />
376+ </ AreaChart >
377+ </ ResponsiveContainer >
378+ </ div >
379+ ) }
380+
381+ { /* Token Volume */ }
382+ { breakdown && breakdown . daily . length > 0 && (
383+ < div className = "card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4" >
384+ < h3 className = "text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4" >
385+ < BarChart className = "w-4 h-4 text-indigo-500" />
386+ Token Volume
387+ </ h3 >
388+ < ResponsiveContainer width = "100%" height = { 200 } >
389+ < RechartsBarChart
390+ data = { breakdown . daily . map ( ( d ) => ( {
391+ ...d ,
392+ date : new Date ( d . date ) . toLocaleDateString ( 'en' , {
393+ month : 'short' ,
394+ day : 'numeric' ,
395+ } ) ,
396+ } ) ) }
397+ >
398+ < CartesianGrid
399+ strokeDasharray = "3 3"
400+ stroke = "var(--color-border, #334155)"
401+ opacity = { 0.4 }
402+ />
403+ < XAxis
404+ dataKey = "date"
405+ tick = { { fontSize : 11 } }
406+ stroke = "var(--color-text-muted, #94a3b8)"
407+ />
408+ < YAxis
409+ tick = { { fontSize : 11 } }
410+ stroke = "var(--color-text-muted, #94a3b8)"
411+ tickFormatter = { ( v ) => ( v >= 1000 ? `${ ( v / 1000 ) . toFixed ( 0 ) } K` : v ) }
412+ />
413+ < Tooltip
414+ contentStyle = { {
415+ background : 'var(--color-bg-secondary, #1e293b)' ,
416+ border : '1px solid var(--color-border, #334155)' ,
417+ borderRadius : '8px' ,
418+ fontSize : '12px' ,
419+ } }
420+ />
421+ < Legend iconType = "circle" iconSize = { 8 } wrapperStyle = { { fontSize : 11 } } />
422+ < Bar
423+ dataKey = "inputTokens"
424+ name = "Input"
425+ fill = "#6366f1"
426+ radius = { [ 3 , 3 , 0 , 0 ] }
427+ />
428+ < Bar
429+ dataKey = "outputTokens"
430+ name = "Output"
431+ fill = "#a855f7"
432+ radius = { [ 3 , 3 , 0 , 0 ] }
433+ />
434+ </ RechartsBarChart >
435+ </ ResponsiveContainer >
338436 </ div >
339437 ) }
340438 </ div >
341439 ) }
342440
343441 { activeTab === 'breakdown' && breakdown && (
344442 < div className = "space-y-6" >
345- { /* Provider Breakdown */ }
443+ { /* Provider Cost Distribution */ }
444+ { breakdown . byProvider . filter ( ( p ) => p . cost > 0 ) . length > 0 && (
445+ < div className = "card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4" >
446+ < h3 className = "text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4" >
447+ < DollarSign className = "w-4 h-4 text-pink-500" />
448+ Cost Distribution
449+ </ h3 >
450+ < div className = "flex items-center gap-8" >
451+ < div className = "w-48 h-48 flex-shrink-0" >
452+ < ResponsiveContainer width = "100%" height = "100%" >
453+ < PieChart >
454+ < Pie
455+ data = { breakdown . byProvider
456+ . filter ( ( p ) => p . cost > 0 )
457+ . map ( ( p ) => ( {
458+ name : p . provider ,
459+ value : Math . round ( p . cost * 100 ) / 100 ,
460+ } ) ) }
461+ cx = "50%"
462+ cy = "50%"
463+ innerRadius = "50%"
464+ outerRadius = "85%"
465+ paddingAngle = { 2 }
466+ dataKey = "value"
467+ stroke = "none"
468+ >
469+ { breakdown . byProvider
470+ . filter ( ( p ) => p . cost > 0 )
471+ . map ( ( _ , i ) => (
472+ < Cell
473+ key = { i }
474+ fill = {
475+ [
476+ '#6366f1' ,
477+ '#8b5cf6' ,
478+ '#ec4899' ,
479+ '#f97316' ,
480+ '#22c55e' ,
481+ '#06b6d4' ,
482+ '#3b82f6' ,
483+ '#eab308' ,
484+ ] [ i % 8 ]
485+ }
486+ />
487+ ) ) }
488+ </ Pie >
489+ < Tooltip
490+ contentStyle = { {
491+ background : 'var(--color-bg-secondary, #1e293b)' ,
492+ border : '1px solid var(--color-border, #334155)' ,
493+ borderRadius : '8px' ,
494+ fontSize : '12px' ,
495+ } }
496+ formatter = { ( value : unknown ) => [ `$${ Number ( value ) . toFixed ( 4 ) } ` , 'Cost' ] }
497+ />
498+ </ PieChart >
499+ </ ResponsiveContainer >
500+ </ div >
501+ < div className = "space-y-2 min-w-0" >
502+ { breakdown . byProvider
503+ . filter ( ( p ) => p . cost > 0 )
504+ . map ( ( p , i ) => (
505+ < div key = { p . provider } className = "flex items-center gap-2 text-sm" >
506+ < span
507+ className = "w-3 h-3 rounded-full flex-shrink-0"
508+ style = { {
509+ background : [
510+ '#6366f1' ,
511+ '#8b5cf6' ,
512+ '#ec4899' ,
513+ '#f97316' ,
514+ '#22c55e' ,
515+ '#06b6d4' ,
516+ '#3b82f6' ,
517+ '#eab308' ,
518+ ] [ i % 8 ] ,
519+ } }
520+ />
521+ < span className = "text-text-muted dark:text-dark-text-muted capitalize truncate" >
522+ { p . provider }
523+ </ span >
524+ < span className = "ml-auto font-medium text-text-primary dark:text-dark-text-primary whitespace-nowrap" >
525+ { p . costFormatted }
526+ </ span >
527+ < span className = "text-text-muted dark:text-dark-text-muted text-xs w-12 text-right" >
528+ { p . percentOfTotal . toFixed ( 0 ) } %
529+ </ span >
530+ </ div >
531+ ) ) }
532+ </ div >
533+ </ div >
534+ </ div >
535+ ) }
536+
537+ { /* Provider Breakdown List */ }
346538 < div className = "card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border" >
347539 < div className = "p-4 border-b border-border dark:border-dark-border" >
348- < h3 className = "text-lg font-medium text-text-primary dark:text-dark-text-primary" >
349- By Provider
540+ < h3 className = "text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2" >
541+ < Layers className = "w-4 h-4 text-orange-500" />
542+ Provider Details
350543 </ h3 >
351544 </ div >
352545 < div className = "divide-y divide-border dark:divide-dark-border" >
0 commit comments