@@ -33,44 +33,102 @@ export interface UseSparklinesReturn {
3333 rpmSparkline : SparklineBundle | null ;
3434 tpmSparkline : SparklineBundle | null ;
3535 costSparkline : SparklineBundle | null ;
36+ chunksSparkline : SparklineBundle | null ;
37+ trafficSparkline : SparklineBundle | null ;
3638}
3739
3840export function useSparklines ( { usage, loading, nowMs } : UseSparklinesOptions ) : UseSparklinesReturn {
3941 const lastHourSeries = useMemo ( ( ) => {
40- if ( ! usage ) return { labels : [ ] , requests : [ ] , tokens : [ ] } ;
41- if ( ! Number . isFinite ( nowMs ) || nowMs <= 0 ) {
42- return { labels : [ ] , requests : [ ] , tokens : [ ] } ;
42+ if ( ! usage ) {
43+ return {
44+ labels : [ ] ,
45+ requests : [ ] ,
46+ tokens : [ ] ,
47+ chunks : [ ] ,
48+ traffic : [ ]
49+ } ;
4350 }
4451 const details = collectUsageDetails ( usage ) ;
45- if ( ! details . length ) return { labels : [ ] , requests : [ ] , tokens : [ ] } ;
52+ if ( ! details . length ) {
53+ return {
54+ labels : [ ] ,
55+ requests : [ ] ,
56+ tokens : [ ] ,
57+ chunks : [ ] ,
58+ traffic : [ ]
59+ } ;
60+ }
61+
62+ const minTimestamp = details . reduce ( ( min , detail ) => {
63+ const timestamp = detail . __timestampMs ?? 0 ;
64+ return Number . isFinite ( timestamp ) && timestamp > 0 ? Math . min ( min , timestamp ) : min ;
65+ } , Number . POSITIVE_INFINITY ) ;
66+ const maxTimestamp = details . reduce ( ( max , detail ) => {
67+ const timestamp = detail . __timestampMs ?? 0 ;
68+ return Number . isFinite ( timestamp ) && timestamp > 0 ? Math . max ( max , timestamp ) : max ;
69+ } , 0 ) ;
70+ const fallbackNow = Number . isFinite ( nowMs ) && nowMs > 0 ? nowMs : 0 ;
71+ const windowEnd = maxTimestamp > 0 ? maxTimestamp : fallbackNow ;
72+ if ( ! Number . isFinite ( minTimestamp ) || minTimestamp <= 0 || ! Number . isFinite ( windowEnd ) || windowEnd <= 0 ) {
73+ return {
74+ labels : [ ] ,
75+ requests : [ ] ,
76+ tokens : [ ] ,
77+ chunks : [ ] ,
78+ traffic : [ ]
79+ } ;
80+ }
4681
47- const windowMinutes = 60 ;
48- const now = nowMs ;
49- const windowStart = now - windowMinutes * 60 * 1000 ;
50- const requestBuckets = new Array ( windowMinutes ) . fill ( 0 ) ;
51- const tokenBuckets = new Array ( windowMinutes ) . fill ( 0 ) ;
82+ const maxBuckets = 60 ;
83+ const spanMs = Math . max ( 1 , windowEnd - minTimestamp + 1 ) ;
84+ const bucketMs = Math . max ( 60000 , Math . ceil ( spanMs / maxBuckets ) ) ;
85+ const bucketCount = Math . max ( 1 , Math . min ( maxBuckets , Math . ceil ( spanMs / bucketMs ) ) ) ;
86+ const windowStart = windowEnd - bucketMs * bucketCount ;
87+ const requestBuckets = new Array ( bucketCount ) . fill ( 0 ) ;
88+ const tokenBuckets = new Array ( bucketCount ) . fill ( 0 ) ;
89+ const chunkBuckets = new Array ( bucketCount ) . fill ( 0 ) ;
90+ const trafficBuckets = new Array ( bucketCount ) . fill ( 0 ) ;
5291
5392 details . forEach ( ( detail ) => {
5493 const timestamp = detail . __timestampMs ?? 0 ;
55- if ( ! Number . isFinite ( timestamp ) || timestamp < windowStart || timestamp > now ) {
94+ if ( ! Number . isFinite ( timestamp ) || timestamp < windowStart || timestamp > windowEnd ) {
5695 return ;
5796 }
58- const minuteIndex = Math . min (
59- windowMinutes - 1 ,
60- Math . floor ( ( timestamp - windowStart ) / 60000 )
97+ const bucketIndex = Math . min (
98+ bucketCount - 1 ,
99+ Math . max ( 0 , Math . floor ( ( timestamp - windowStart ) / bucketMs ) )
61100 ) ;
62- requestBuckets [ minuteIndex ] += 1 ;
63- tokenBuckets [ minuteIndex ] += extractTotalTokens ( detail ) ;
101+ requestBuckets [ bucketIndex ] += 1 ;
102+ tokenBuckets [ bucketIndex ] += extractTotalTokens ( detail ) ;
103+ chunkBuckets [ bucketIndex ] +=
104+ typeof detail . chunk_count === 'number' && Number . isFinite ( detail . chunk_count )
105+ ? Math . max ( detail . chunk_count , 0 )
106+ : 0 ;
107+ const responseBytes =
108+ typeof detail . response_bytes === 'number' && Number . isFinite ( detail . response_bytes )
109+ ? Math . max ( detail . response_bytes , 0 )
110+ : 0 ;
111+ const apiResponseBytes =
112+ typeof detail . api_response_bytes === 'number' && Number . isFinite ( detail . api_response_bytes )
113+ ? Math . max ( detail . api_response_bytes , 0 )
114+ : 0 ;
115+ trafficBuckets [ bucketIndex ] += responseBytes + apiResponseBytes ;
64116 } ) ;
65117
66118 const labels = requestBuckets . map ( ( _ , idx ) => {
67- const date = new Date ( windowStart + ( idx + 1 ) * 60000 ) ;
119+ const date = new Date ( windowStart + ( idx + 1 ) * bucketMs ) ;
68120 const h = date . getHours ( ) . toString ( ) . padStart ( 2 , '0' ) ;
69121 const m = date . getMinutes ( ) . toString ( ) . padStart ( 2 , '0' ) ;
70122 return `${ h } :${ m } ` ;
71123 } ) ;
72124
73- return { labels, requests : requestBuckets , tokens : tokenBuckets } ;
125+ return {
126+ labels,
127+ requests : requestBuckets ,
128+ tokens : tokenBuckets ,
129+ chunks : chunkBuckets ,
130+ traffic : trafficBuckets
131+ } ;
74132 } , [ nowMs , usage ] ) ;
75133
76134 const buildSparkline = useCallback (
@@ -155,11 +213,33 @@ export function useSparklines({ usage, loading, nowMs }: UseSparklinesOptions):
155213 [ buildSparkline , lastHourSeries . labels , lastHourSeries . tokens ]
156214 ) ;
157215
216+ const chunksSparkline = useMemo (
217+ ( ) =>
218+ buildSparkline (
219+ { labels : lastHourSeries . labels , data : lastHourSeries . chunks } ,
220+ '#0ea5e9' ,
221+ 'rgba(14, 165, 233, 0.18)'
222+ ) ,
223+ [ buildSparkline , lastHourSeries . chunks , lastHourSeries . labels ]
224+ ) ;
225+
226+ const trafficSparkline = useMemo (
227+ ( ) =>
228+ buildSparkline (
229+ { labels : lastHourSeries . labels , data : lastHourSeries . traffic } ,
230+ '#14b8a6' ,
231+ 'rgba(20, 184, 166, 0.18)'
232+ ) ,
233+ [ buildSparkline , lastHourSeries . labels , lastHourSeries . traffic ]
234+ ) ;
235+
158236 return {
159237 requestsSparkline,
160238 tokensSparkline,
161239 rpmSparkline,
162240 tpmSparkline,
163- costSparkline
241+ costSparkline,
242+ chunksSparkline,
243+ trafficSparkline
164244 } ;
165245}
0 commit comments