@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react';
22import { useTranslation } from 'react-i18next' ;
33import { Card } from '@/components/ui/Card' ;
44import {
5+ LATENCY_SOURCE_FIELD ,
56 formatCompactNumber ,
67 formatDurationMs ,
78 formatUsd ,
@@ -35,6 +36,10 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
3536 const { t } = useTranslation ( ) ;
3637 const [ sortKey , setSortKey ] = useState < SortKey > ( 'requests' ) ;
3738 const [ sortDir , setSortDir ] = useState < SortDir > ( 'desc' ) ;
39+ const latencyHint = t ( 'usage_stats.latency_unit_hint' , {
40+ field : LATENCY_SOURCE_FIELD ,
41+ unit : t ( 'usage_stats.duration_unit_ms' ) ,
42+ } ) ;
3843
3944 const handleSort = ( key : SortKey ) => {
4045 if ( sortKey === key ) {
@@ -66,129 +71,146 @@ export function ModelStatsCard({ modelStats, loading, hasPrices }: ModelStatsCar
6671 return list ;
6772 } , [ modelStats , sortKey , sortDir ] ) ;
6873
69- const arrow = ( key : SortKey ) =>
70- sortKey === key ? ( sortDir === 'asc' ? ' ▲' : ' ▼' ) : '' ;
74+ const arrow = ( key : SortKey ) => ( sortKey === key ? ( sortDir === 'asc' ? ' ▲' : ' ▼' ) : '' ) ;
7175 const ariaSort = ( key : SortKey ) : 'none' | 'ascending' | 'descending' =>
7276 sortKey === key ? ( sortDir === 'asc' ? 'ascending' : 'descending' ) : 'none' ;
77+ const hasLatencyData = sorted . some ( ( stat ) => stat . latencySampleCount > 0 ) ;
7378
7479 return (
7580 < Card title = { t ( 'usage_stats.models' ) } className = { styles . detailsFixedCard } >
7681 { loading ? (
7782 < div className = { styles . hint } > { t ( 'common.loading' ) } </ div >
7883 ) : sorted . length > 0 ? (
79- < div className = { styles . detailsScroll } >
80- < div className = { styles . tableWrapper } >
81- < table className = { styles . table } >
82- < thead >
83- < tr >
84- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'model' ) } >
85- < button
86- type = "button"
87- className = { styles . sortHeaderButton }
88- onClick = { ( ) => handleSort ( 'model' ) }
89- >
90- { t ( 'usage_stats.model_name' ) } { arrow ( 'model' ) }
91- </ button >
92- </ th >
93- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'requests' ) } >
94- < button
95- type = "button"
96- className = { styles . sortHeaderButton }
97- onClick = { ( ) => handleSort ( 'requests' ) }
98- >
99- { t ( 'usage_stats.requests_count' ) } { arrow ( 'requests' ) }
100- </ button >
101- </ th >
102- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'tokens' ) } >
103- < button
104- type = "button"
105- className = { styles . sortHeaderButton }
106- onClick = { ( ) => handleSort ( 'tokens' ) }
107- >
108- { t ( 'usage_stats.tokens_count' ) } { arrow ( 'tokens' ) }
109- </ button >
110- </ th >
111- < th
112- className = { styles . sortableHeader }
113- aria-sort = { ariaSort ( 'averageLatencyMs' ) }
114- >
115- < button
116- type = "button"
117- className = { styles . sortHeaderButton }
118- onClick = { ( ) => handleSort ( 'averageLatencyMs' ) }
119- >
120- { t ( 'usage_stats.avg_time' ) } { arrow ( 'averageLatencyMs' ) }
121- </ button >
122- </ th >
123- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'totalLatencyMs' ) } >
124- < button
125- type = "button"
126- className = { styles . sortHeaderButton }
127- onClick = { ( ) => handleSort ( 'totalLatencyMs' ) }
128- >
129- { t ( 'usage_stats.total_time' ) } { arrow ( 'totalLatencyMs' ) }
130- </ button >
131- </ th >
132- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'successRate' ) } >
133- < button
134- type = "button"
135- className = { styles . sortHeaderButton }
136- onClick = { ( ) => handleSort ( 'successRate' ) }
137- >
138- { t ( 'usage_stats.success_rate' ) } { arrow ( 'successRate' ) }
139- </ button >
140- </ th >
141- { hasPrices && (
142- < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'cost' ) } >
84+ < >
85+ { hasLatencyData && < div className = { styles . detailsNote } > { latencyHint } </ div > }
86+ < div className = { styles . detailsScroll } >
87+ < div className = { styles . tableWrapper } >
88+ < table className = { styles . table } >
89+ < thead >
90+ < tr >
91+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'model' ) } >
14392 < button
14493 type = "button"
14594 className = { styles . sortHeaderButton }
146- onClick = { ( ) => handleSort ( 'cost ' ) }
95+ onClick = { ( ) => handleSort ( 'model ' ) }
14796 >
148- { t ( 'usage_stats.total_cost' ) } { arrow ( 'cost' ) }
97+ { t ( 'usage_stats.model_name' ) }
98+ { arrow ( 'model' ) }
14999 </ button >
150100 </ th >
151- ) }
152- </ tr >
153- </ thead >
154- < tbody >
155- { sorted . map ( ( stat ) => (
156- < tr key = { stat . model } >
157- < td className = { styles . modelCell } > { stat . model } </ td >
158- < td >
159- < span className = { styles . requestCountCell } >
160- < span > { stat . requests . toLocaleString ( ) } </ span >
161- < span className = { styles . requestBreakdown } >
162- (< span className = { styles . statSuccess } > { stat . successCount . toLocaleString ( ) } </ span > { ' ' }
163- < span className = { styles . statFailure } > { stat . failureCount . toLocaleString ( ) } </ span > )
164- </ span >
165- </ span >
166- </ td >
167- < td > { formatCompactNumber ( stat . tokens ) } </ td >
168- < td className = { styles . durationCell } >
169- { formatDurationMs ( stat . averageLatencyMs ) }
170- </ td >
171- < td className = { styles . durationCell } > { formatDurationMs ( stat . totalLatencyMs ) } </ td >
172- < td >
173- < span
174- className = {
175- stat . successRate >= 95
176- ? styles . statSuccess
177- : stat . successRate >= 80
178- ? styles . statNeutral
179- : styles . statFailure
180- }
101+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'requests' ) } >
102+ < button
103+ type = "button"
104+ className = { styles . sortHeaderButton }
105+ onClick = { ( ) => handleSort ( 'requests' ) }
106+ >
107+ { t ( 'usage_stats.requests_count' ) }
108+ { arrow ( 'requests' ) }
109+ </ button >
110+ </ th >
111+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'tokens' ) } >
112+ < button
113+ type = "button"
114+ className = { styles . sortHeaderButton }
115+ onClick = { ( ) => handleSort ( 'tokens' ) }
116+ >
117+ { t ( 'usage_stats.tokens_count' ) }
118+ { arrow ( 'tokens' ) }
119+ </ button >
120+ </ th >
121+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'averageLatencyMs' ) } >
122+ < button
123+ type = "button"
124+ className = { styles . sortHeaderButton }
125+ onClick = { ( ) => handleSort ( 'averageLatencyMs' ) }
126+ title = { latencyHint }
181127 >
182- { stat . successRate . toFixed ( 1 ) } %
183- </ span >
184- </ td >
185- { hasPrices && < td > { stat . cost > 0 ? formatUsd ( stat . cost ) : '--' } </ td > }
128+ { t ( 'usage_stats.avg_time' ) }
129+ { arrow ( 'averageLatencyMs' ) }
130+ </ button >
131+ </ th >
132+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'totalLatencyMs' ) } >
133+ < button
134+ type = "button"
135+ className = { styles . sortHeaderButton }
136+ onClick = { ( ) => handleSort ( 'totalLatencyMs' ) }
137+ title = { latencyHint }
138+ >
139+ { t ( 'usage_stats.total_time' ) }
140+ { arrow ( 'totalLatencyMs' ) }
141+ </ button >
142+ </ th >
143+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'successRate' ) } >
144+ < button
145+ type = "button"
146+ className = { styles . sortHeaderButton }
147+ onClick = { ( ) => handleSort ( 'successRate' ) }
148+ >
149+ { t ( 'usage_stats.success_rate' ) }
150+ { arrow ( 'successRate' ) }
151+ </ button >
152+ </ th >
153+ { hasPrices && (
154+ < th className = { styles . sortableHeader } aria-sort = { ariaSort ( 'cost' ) } >
155+ < button
156+ type = "button"
157+ className = { styles . sortHeaderButton }
158+ onClick = { ( ) => handleSort ( 'cost' ) }
159+ >
160+ { t ( 'usage_stats.total_cost' ) }
161+ { arrow ( 'cost' ) }
162+ </ button >
163+ </ th >
164+ ) }
186165 </ tr >
187- ) ) }
188- </ tbody >
189- </ table >
166+ </ thead >
167+ < tbody >
168+ { sorted . map ( ( stat ) => (
169+ < tr key = { stat . model } >
170+ < td className = { styles . modelCell } > { stat . model } </ td >
171+ < td >
172+ < span className = { styles . requestCountCell } >
173+ < span > { stat . requests . toLocaleString ( ) } </ span >
174+ < span className = { styles . requestBreakdown } >
175+ (
176+ < span className = { styles . statSuccess } >
177+ { stat . successCount . toLocaleString ( ) }
178+ </ span > { ' ' }
179+ < span className = { styles . statFailure } >
180+ { stat . failureCount . toLocaleString ( ) }
181+ </ span >
182+ )
183+ </ span >
184+ </ span >
185+ </ td >
186+ < td > { formatCompactNumber ( stat . tokens ) } </ td >
187+ < td className = { styles . durationCell } >
188+ { formatDurationMs ( stat . averageLatencyMs ) }
189+ </ td >
190+ < td className = { styles . durationCell } >
191+ { formatDurationMs ( stat . totalLatencyMs ) }
192+ </ td >
193+ < td >
194+ < span
195+ className = {
196+ stat . successRate >= 95
197+ ? styles . statSuccess
198+ : stat . successRate >= 80
199+ ? styles . statNeutral
200+ : styles . statFailure
201+ }
202+ >
203+ { stat . successRate . toFixed ( 1 ) } %
204+ </ span >
205+ </ td >
206+ { hasPrices && < td > { stat . cost > 0 ? formatUsd ( stat . cost ) : '--' } </ td > }
207+ </ tr >
208+ ) ) }
209+ </ tbody >
210+ </ table >
211+ </ div >
190212 </ div >
191- </ div >
213+ </ >
192214 ) : (
193215 < div className = { styles . hint } > { t ( 'usage_stats.no_data' ) } </ div >
194216 ) }
0 commit comments