import { useRef, useState, useCallback } from 'react' import useDownloads, { useDownloadsProps } from '~/hooks/useDownloads' import { extractFileName, formatBytes } from '~/lib/util' import StyledSectionHeader from './StyledSectionHeader' import { IconAlertTriangle, IconX, IconLoader2 } from '@tabler/icons-react' import api from '~/lib/api' interface ActiveDownloadProps { filetype?: useDownloadsProps['filetype'] withHeader?: boolean } function formatSpeed(bytesPerSec: number): string { if (bytesPerSec <= 0) return '0 B/s' if (bytesPerSec < 1024) return `${Math.round(bytesPerSec)} B/s` if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(1)} KB/s` return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s` } type DownloadStatus = 'queued' | 'active' | 'stalled' | 'failed' function getDownloadStatus(download: { progress: number lastProgressTime?: number status?: string }): DownloadStatus { if (download.status === 'failed') return 'failed' if (download.status === 'waiting' || download.status === 'delayed') return 'queued' // Fallback heuristic for model jobs and in-flight jobs from before this deploy if (download.progress === 0 && !download.lastProgressTime) return 'queued' if (download.lastProgressTime) { const elapsed = Date.now() - download.lastProgressTime if (elapsed > 60_000) return 'stalled' } return 'active' } const ActiveDownloads = ({ filetype, withHeader = false }: ActiveDownloadProps) => { const { data: downloads, invalidate } = useDownloads({ filetype }) const [cancellingJobs, setCancellingJobs] = useState>(new Set()) const [confirmingCancel, setConfirmingCancel] = useState(null) // Track previous downloadedBytes for speed calculation const prevBytesRef = useRef>(new Map()) const speedRef = useRef>(new Map()) const getSpeed = useCallback( (jobId: string, currentBytes?: number): number => { if (!currentBytes || currentBytes <= 0) return 0 const prev = prevBytesRef.current.get(jobId) const now = Date.now() if (prev && prev.bytes > 0 && currentBytes > prev.bytes) { const deltaBytes = currentBytes - prev.bytes const deltaSec = (now - prev.time) / 1000 if (deltaSec > 0) { const instantSpeed = deltaBytes / deltaSec // Simple moving average (last 5 samples) const samples = speedRef.current.get(jobId) || [] samples.push(instantSpeed) if (samples.length > 5) samples.shift() speedRef.current.set(jobId, samples) const avg = samples.reduce((a, b) => a + b, 0) / samples.length prevBytesRef.current.set(jobId, { bytes: currentBytes, time: now }) return avg } } // Only set initial observation; never advance timestamp when bytes unchanged if (!prev) { prevBytesRef.current.set(jobId, { bytes: currentBytes, time: now }) } return speedRef.current.get(jobId)?.at(-1) || 0 }, [] ) const handleDismiss = async (jobId: string) => { await api.removeDownloadJob(jobId) invalidate() } const handleCancel = async (jobId: string) => { setCancellingJobs((prev) => new Set(prev).add(jobId)) setConfirmingCancel(null) try { await api.cancelDownloadJob(jobId) // Clean up speed tracking refs prevBytesRef.current.delete(jobId) speedRef.current.delete(jobId) } finally { setCancellingJobs((prev) => { const next = new Set(prev) next.delete(jobId) return next }) invalidate() } } return ( <> {withHeader && }
{downloads && downloads.length > 0 ? ( downloads.map((download) => { const filename = extractFileName(download.filepath) || download.url const status = getDownloadStatus(download) const speed = getSpeed(download.jobId, download.downloadedBytes) const isCancelling = cancellingJobs.has(download.jobId) const isConfirming = confirmingCancel === download.jobId return (
{status === 'failed' ? (

{download.title || filename}

{download.title && (

{filename}

)}

Download failed{download.failedReason ? `: ${download.failedReason}` : ''}

) : (
{/* Title + Cancel button row */}

{download.title || filename}

{download.title && (
{filename} {download.filetype}
)} {!download.title && download.filetype && ( {download.filetype} )}
{isConfirming ? (
) : isCancelling ? ( ) : ( )}
{/* Size info */}
{download.downloadedBytes && download.totalBytes ? `${formatBytes(download.downloadedBytes, 1)} / ${formatBytes(download.totalBytes, 1)}` : `${download.progress}% / 100%`}
{/* Progress bar */}
15 ? 'left-2 text-white drop-shadow-md' : 'right-2 text-desert-green' }`} > {Math.round(download.progress)}%
{/* Status indicator */}
{status === 'queued' && ( <>
Waiting... )} {status === 'active' && ( <>
Downloading...{speed > 0 ? ` ${formatSpeed(speed)}` : ''} )} {status === 'stalled' && download.lastProgressTime && ( <>
No data received for{' '} {Math.floor((Date.now() - download.lastProgressTime) / 60_000)}m... )}
)}
) }) ) : (

No active downloads

)}
) } export default ActiveDownloads