Skip to content

Commit 4bfeb8d

Browse files
committed
[console] improve download bar
1 parent 6abfea0 commit 4bfeb8d

2 files changed

Lines changed: 112 additions & 45 deletions

File tree

  • web-console

web-console/backend/internal/handlers/models.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,7 @@ type ModelInfoFromConfigMap struct {
506506
Phase string `json:"phase"`
507507
TotalBytes uint64 `json:"totalBytes"`
508508
CompletedBytes uint64 `json:"completedBytes"`
509-
BytesPerSecond float64 `json:"bytesPerSecond"`
510-
RemainingTime float64 `json:"remainingTime"`
509+
SpeedBytesPerSec float64 `json:"speedBytesPerSec"`
511510
} `json:"progress,omitempty"`
512511
}
513512

@@ -538,12 +537,14 @@ func (h *ModelsHandler) GetProgress(c *gin.Context) {
538537
}
539538

540539
// Look for the specific model in this ConfigMap's data
541-
// The key format can be just the model name for ClusterBaseModels
542-
// or namespace/name for namespaced BaseModels
540+
// Key formats from constants.GetModelConfigMapKey:
541+
// - ClusterBaseModel: "clusterbasemodel.{model_name}"
542+
// - BaseModel: "{namespace}.basemodel.{model_name}"
543543
for key, value := range cm.Data {
544544
// Check if this entry is for our model
545-
// Handle both "modelName" and potentially "namespace/modelName" formats
546-
if key != modelName && key != "default/"+modelName {
545+
clusterKey := "clusterbasemodel." + modelName
546+
defaultNsKey := "default.basemodel." + modelName
547+
if key != clusterKey && key != defaultNsKey {
547548
continue
548549
}
549550

@@ -563,13 +564,20 @@ func (h *ModelsHandler) GetProgress(c *gin.Context) {
563564
percentage = float64(modelInfo.Progress.CompletedBytes) / float64(modelInfo.Progress.TotalBytes) * 100
564565
}
565566

567+
// Calculate remaining time from speed and remaining bytes
568+
remainingTime := float64(0)
569+
if modelInfo.Progress.SpeedBytesPerSec > 0 {
570+
remainingBytes := modelInfo.Progress.TotalBytes - modelInfo.Progress.CompletedBytes
571+
remainingTime = float64(remainingBytes) / modelInfo.Progress.SpeedBytesPerSec
572+
}
573+
566574
progressList = append(progressList, NodeDownloadProgress{
567575
Node: nodeName,
568576
Phase: modelInfo.Progress.Phase,
569577
TotalBytes: modelInfo.Progress.TotalBytes,
570578
CompletedBytes: modelInfo.Progress.CompletedBytes,
571-
BytesPerSecond: modelInfo.Progress.BytesPerSecond,
572-
RemainingTime: modelInfo.Progress.RemainingTime,
579+
BytesPerSecond: modelInfo.Progress.SpeedBytesPerSec,
580+
RemainingTime: remainingTime,
573581
Percentage: percentage,
574582
})
575583
}

web-console/frontend/src/app/(dashboard)/models/[name]/page.tsx

Lines changed: 96 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
2740
export 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

Comments
 (0)