@@ -24,6 +24,19 @@ function formatBytes(bytes: number): string {
2424 return parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) + ' ' + sizes [ i ]
2525}
2626
27+ // Helper to format time duration
28+ function formatDuration ( seconds : number ) : string {
29+ if ( seconds < 60 ) return `${ Math . ceil ( seconds ) } s`
30+ if ( seconds < 3600 ) {
31+ const mins = Math . floor ( seconds / 60 )
32+ const secs = Math . ceil ( seconds % 60 )
33+ return `${ mins } m ${ secs } s`
34+ }
35+ const hours = Math . floor ( seconds / 3600 )
36+ const mins = Math . floor ( ( seconds % 3600 ) / 60 )
37+ return `${ hours } h ${ mins } m`
38+ }
39+
2740export default function ModelDetailPage ( ) {
2841 const params = useParams ( )
2942 const router = useRouter ( )
@@ -149,45 +162,91 @@ export default function ModelDetailPage() {
149162 </ div >
150163
151164 { /* Download Progress - shown when downloading */ }
152- { isDownloading && progressData && progressData . progress . length > 0 && (
153- < div className = "mt-6 pt-6 border-t border-gray-200" >
154- < h3 className = "text-sm font-medium text-gray-900 mb-3" > Download Progress</ h3 >
155- < div className = "space-y-2" >
156- { progressData . progress . map ( ( progress ) => (
157- < div key = { progress . node } className = "flex items-center gap-3 text-sm" >
158- < span className = "text-gray-500 w-28 truncate font-mono text-xs" >
159- { progress . node }
160- </ span >
161- < div className = "flex-1 bg-gray-200 rounded-full h-2" >
162- < div
163- className = "bg-blue-600 h-2 rounded-full transition-all duration-300"
164- style = { { width : `${ Math . min ( progress . percentage , 100 ) } %` } }
165- />
166- </ div >
167- < span className = "text-gray-700 w-12 text-right" >
168- { progress . percentage . toFixed ( 1 ) } %
169- </ span >
170- < span className = "text-gray-500 w-20 text-right text-xs" >
171- { formatBytes ( progress . bytesPerSecond ) } /s
172- </ span >
173- { progress . remainingTime > 0 && (
174- < span className = "text-gray-400 w-16 text-xs" >
175- { Math . ceil ( progress . remainingTime ) } s
176- </ span >
177- ) }
165+ { isDownloading && progressData && progressData . progress . length > 0 && ( ( ) => {
166+ // Calculate aggregate stats
167+ const avgPercentage = progressData . progress . reduce ( ( sum , p ) => sum + p . percentage , 0 ) / progressData . progress . length
168+ const minPercentage = Math . min ( ...progressData . progress . map ( p => p . percentage ) )
169+ const totalSpeed = progressData . progress . reduce ( ( sum , p ) => sum + p . bytesPerSecond , 0 )
170+ const maxEta = Math . max ( ...progressData . progress . map ( p => p . remainingTime ) )
171+ const firstProgress = progressData . progress [ 0 ]
172+
173+ return (
174+ < div className = "mt-6 pt-6 border-t border-gray-200" >
175+ < div className = "flex items-center justify-between mb-4" >
176+ < h3 className = "text-sm font-medium text-gray-900" > Download Progress</ h3 >
177+ < div className = "flex items-center gap-4 text-xs text-gray-500" >
178+ < span > { progressData . progress . length } nodes</ span >
179+ < span className = "text-blue-600 font-medium" > { formatBytes ( totalSpeed ) } /s total</ span >
180+ { maxEta > 0 && < span > ETA: { formatDuration ( maxEta ) } </ span > }
178181 </ div >
179- ) ) }
180- </ div >
181- < div className = "mt-2 text-xs text-gray-400" >
182- { progressData . progress [ 0 ] && (
183- < span >
184- { formatBytes ( progressData . progress [ 0 ] . completedBytes ) } /{ ' ' }
185- { formatBytes ( progressData . progress [ 0 ] . totalBytes ) } per node
186- </ span >
187- ) }
182+ </ div >
183+
184+ { /* Aggregate progress bar */ }
185+ < div className = "mb-4 p-3 bg-gray-50 rounded-lg" >
186+ < div className = "flex items-center justify-between mb-2" >
187+ < span className = "text-sm font-medium text-gray-700" > Overall Progress</ span >
188+ < span className = "text-sm font-semibold text-blue-600" > { avgPercentage . toFixed ( 1 ) } %</ span >
189+ </ div >
190+ < div className = "relative h-3 bg-gray-200 rounded-full overflow-hidden" >
191+ < div
192+ className = "absolute inset-y-0 left-0 bg-gradient-to-r from-blue-500 to-blue-600 rounded-full transition-all duration-500"
193+ style = { { width : `${ Math . min ( avgPercentage , 100 ) } %` } }
194+ />
195+ { /* Min progress indicator */ }
196+ < div
197+ className = "absolute inset-y-0 w-0.5 bg-blue-800 opacity-50"
198+ style = { { left : `${ Math . min ( minPercentage , 100 ) } %` } }
199+ title = { `Slowest node: ${ minPercentage . toFixed ( 1 ) } %` }
200+ />
201+ </ div >
202+ < div className = "flex justify-between mt-2 text-xs text-gray-500" >
203+ < span > { formatBytes ( firstProgress ?. completedBytes || 0 ) } / { formatBytes ( firstProgress ?. totalBytes || 0 ) } per node</ span >
204+ < span > Min: { minPercentage . toFixed ( 1 ) } %</ span >
205+ </ div >
206+ </ div >
207+
208+ { /* Per-node progress */ }
209+ < div className = "space-y-2 max-h-64 overflow-y-auto" >
210+ { progressData . progress
211+ . sort ( ( a , b ) => a . percentage - b . percentage ) // Show slowest first
212+ . map ( ( progress ) => {
213+ const pct = Math . min ( progress . percentage , 100 )
214+ const isSlower = progress . percentage < avgPercentage - 5
215+
216+ return (
217+ < div
218+ key = { progress . node }
219+ className = { `flex items-center gap-3 p-2 rounded-lg text-sm ${ isSlower ? 'bg-amber-50' : 'hover:bg-gray-50' } ` }
220+ >
221+ < span className = { `w-28 truncate font-mono text-xs ${ isSlower ? 'text-amber-700' : 'text-gray-600' } ` } >
222+ { progress . node }
223+ </ span >
224+ < div className = "flex-1 relative" >
225+ < div className = "h-2 bg-gray-200 rounded-full overflow-hidden" >
226+ < div
227+ className = { `h-full rounded-full transition-all duration-300 ${
228+ isSlower ? 'bg-amber-500' : 'bg-blue-500'
229+ } `}
230+ style = { { width : `${ pct } %` } }
231+ />
232+ </ div >
233+ </ div >
234+ < span className = { `w-14 text-right font-medium ${ isSlower ? 'text-amber-700' : 'text-gray-700' } ` } >
235+ { progress . percentage . toFixed ( 1 ) } %
236+ </ span >
237+ < span className = "text-gray-500 w-24 text-right text-xs" >
238+ { formatBytes ( progress . bytesPerSecond ) } /s
239+ </ span >
240+ < span className = "text-gray-400 w-16 text-right text-xs" >
241+ { progress . remainingTime > 0 ? formatDuration ( progress . remainingTime ) : '-' }
242+ </ span >
243+ </ div >
244+ )
245+ } ) }
246+ </ div >
188247 </ div >
189- </ div >
190- ) }
248+ )
249+ } ) ( ) }
191250
192251 { /* Nodes Ready/Failed */ }
193252 { model . status ?. nodesReady && (
0 commit comments