import { useRef } from 'react'; import type { ModelId, ModelLoadProgress, StreamState, } from '../../lib/contracts'; import { formatBytes, formatPercent } from '../../lib/format'; interface ComposerActivityProps { modelId: ModelId; streamState: StreamState; progress: ModelLoadProgress; } function ratio(value: number, total: number): number { return total > 0 ? Math.min(1, Math.max(0, value / total)) : 0; } function humanizeStage(value: string | null): string { if (!value) return ''; return value.replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); } export function overallLoadProgress(progress: ModelLoadProgress): number { const stageProgress = Math.min(1, Math.max(0, progress.stageProgress)); const loadedShardBytes = progress.shards.reduce((total, shard) => ( total + (shard.state === 'cached' || shard.state === 'complete' ? shard.totalBytes : Math.min(shard.loadedBytes, shard.totalBytes)) ), 0); const verifiedBytes = progress.shards.reduce((total, shard) => ( total + (shard.state === 'cached' || shard.state === 'complete' ? shard.totalBytes : Math.min(shard.verifiedBytes, shard.totalBytes)) ), 0); const downloadRatio = ratio(Math.max(progress.downloadedBytes, loadedShardBytes), progress.totalBytes); const verifiedRatio = ratio(verifiedBytes, progress.totalBytes); const transferProgress = Math.min(0.9, 0.08 + downloadRatio * 0.57 + verifiedRatio * 0.25); switch (progress.stage) { case 'manifest': return stageProgress * 0.03; case 'cache': return Math.max(0.03 + stageProgress * 0.05, transferProgress); case 'download': case 'verify': return transferProgress; case 'load': return 0.9 + stageProgress * 0.1; case 'ready': return 1; default: return 0; } } export function describeLoadProgress(progress: ModelLoadProgress): { label: string; detail: string } { const shardCount = progress.shards.length; const readyShards = progress.shards.filter((shard) => shard.state === 'cached' || shard.state === 'complete').length; const activeShard = progress.shards.find((shard) => shard.state === 'downloading' || shard.state === 'verifying'); const shardDetail = shardCount > 0 ? `${readyShards} of ${shardCount} shards ready` : 'Preparing shard map'; const byteDetail = progress.totalBytes > 0 ? `${formatBytes(progress.downloadedBytes)} / ${formatBytes(progress.totalBytes)}` : ''; switch (progress.stage) { case 'manifest': return { label: 'Reading model manifest', detail: progress.modelLabel || 'Pinned release metadata' }; case 'cache': return { label: 'Checking local cache', detail: shardCount > 0 ? shardDetail : 'Looking for existing shards' }; case 'download': return { label: activeShard ? `Downloading shard ${activeShard.index + 1} of ${shardCount}` : 'Downloading model shards', detail: [shardDetail, byteDetail].filter(Boolean).join(' · '), }; case 'verify': return { label: activeShard ? `Verifying shard ${activeShard.index + 1} of ${shardCount}` : 'Verifying model shards', detail: [shardDetail, byteDetail].filter(Boolean).join(' · '), }; case 'load': return { label: progress.nativeStage ? humanizeStage(progress.nativeStage) : 'Allocating model', detail: progress.residentBytes > 0 ? `${formatBytes(progress.residentBytes)} resident` : 'Building the local graph', }; default: return { label: 'Preparing model', detail: progress.modelLabel }; } } export function ComposerActivity({ modelId, streamState, progress }: ComposerActivityProps) { const displayedProgress = useRef<{ modelId: ModelId | null; value: number }>({ modelId: null, value: 0 }); const isLoading = streamState === 'loading' && progress.modelId === modelId; if (!isLoading) { displayedProgress.current = { modelId: null, value: 0 }; return null; } const measuredProgress = overallLoadProgress(progress); const overallProgress = displayedProgress.current.modelId === modelId ? Math.max(displayedProgress.current.value, measuredProgress) : measuredProgress; displayedProgress.current = { modelId, value: overallProgress }; const status = describeLoadProgress(progress); const percentage = Math.round(overallProgress * 100); return (
{formatPercent(overallProgress)}
); }