import { useCallback, useRef, useState } from 'react' import useOllamaModelDownloads from '~/hooks/useOllamaModelDownloads' import StyledSectionHeader from './StyledSectionHeader' import StyledModal from './StyledModal' import { IconAlertTriangle, IconLoader2, IconX } from '@tabler/icons-react' import api from '~/lib/api' import { useModals } from '~/context/ModalContext' import { formatBytes } from '~/lib/util' interface ActiveModelDownloadsProps { 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` } const ActiveModelDownloads = ({ withHeader = false }: ActiveModelDownloadsProps) => { const { downloads, removeDownload } = useOllamaModelDownloads() const { openModal, closeAllModals } = useModals() const [cancellingModels, setCancellingModels] = useState>(new Set()) // Track previous downloadedBytes for speed calculation — mirrors the approach in // ActiveDownloads.tsx so content + model downloads feel identical. const prevBytesRef = useRef>(new Map()) const speedRef = useRef>(new Map()) const getSpeed = useCallback((model: string, currentBytes?: number): number => { if (!currentBytes || currentBytes <= 0) return 0 const prev = prevBytesRef.current.get(model) 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(model) || [] samples.push(instantSpeed) if (samples.length > 5) samples.shift() speedRef.current.set(model, samples) const avg = samples.reduce((a, b) => a + b, 0) / samples.length prevBytesRef.current.set(model, { bytes: currentBytes, time: now }) return avg } } // Only set initial observation; never advance timestamp when bytes unchanged if (!prev) { prevBytesRef.current.set(model, { bytes: currentBytes, time: now }) } return speedRef.current.get(model)?.at(-1) || 0 }, []) const runCancel = async (download: { model: string; jobId?: string }) => { // Defensive guard: stale broadcasts during a hot upgrade may not include jobId. // Without it we have nothing to call the cancel API with. if (!download.jobId) return setCancellingModels((prev) => new Set(prev).add(download.model)) try { await api.cancelDownloadJob(download.jobId) // Optimistically clear the entry — the Transmit cancelled broadcast usually // arrives within a second but we don't want to leave the row hanging if it doesn't. removeDownload(download.model) // Clean up speed tracking refs for this model prevBytesRef.current.delete(download.model) speedRef.current.delete(download.model) } finally { setCancellingModels((prev) => { const next = new Set(prev) next.delete(download.model) return next }) } } const confirmCancel = (download: { model: string; jobId?: string }) => { if (!download.jobId) return openModal( { closeAllModals() runCancel(download) }} onCancel={closeAllModals} open={true} confirmText="Cancel Download" cancelText="Keep Downloading" >

Stop downloading {download.model}?

Any data already downloaded will remain on disk. If you re-download this model later, it will resume from where it left off rather than starting over.

, 'confirm-cancel-model-download-modal' ) } return ( <> {withHeader && }
{downloads && downloads.length > 0 ? ( downloads.map((download) => { const isCancelling = cancellingModels.has(download.model) const canCancel = !!download.jobId && !download.error const speed = getSpeed(download.model, download.downloadedBytes) const hasBytes = !!(download.downloadedBytes && download.totalBytes) return (
{download.error ? (

{download.model}

{download.error}

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

{download.model}

ollama
{canCancel && ( isCancelling ? ( ) : ( ) )}
{/* Size info */}
{hasBytes ? `${formatBytes(download.downloadedBytes!, 1)} / ${formatBytes(download.totalBytes!, 1)}` : `${download.percent.toFixed(1)}% / 100%`}
{/* Progress bar */}
15 ? 'left-2 text-white drop-shadow-md' : 'right-2 text-desert-green' }`} > {Math.round(download.percent)}%
{/* Status indicator */}
Downloading...{speed > 0 ? ` ${formatSpeed(speed)}` : ''}
)}
) }) ) : (

No active model downloads

)}
) } export default ActiveModelDownloads