Spaces:
Runtime error
Runtime error
File size: 4,265 Bytes
442a1fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import { Loader2, XCircle } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import type { ModelProgress as ModelProgressType } from '@/lib/api/types';
import { useServerStore } from '@/stores/serverStore';
interface ModelProgressProps {
modelName: string;
displayName: string;
/** Only connect to SSE when actively downloading - prevents connection exhaustion */
isDownloading?: boolean;
}
export function ModelProgress({
modelName,
displayName,
isDownloading = false,
}: ModelProgressProps) {
const [progress, setProgress] = useState<ModelProgressType | null>(null);
const serverUrl = useServerStore((state) => state.serverUrl);
useEffect(() => {
// IMPORTANT: Only connect to SSE when this specific model is downloading
// Opening SSE connections for all models exhausts HTTP/1.1 connection limits (6 per origin)
// which causes other fetches (like the download trigger) to be queued/blocked
if (!serverUrl || !isDownloading) {
return;
}
console.log(`[ModelProgress] Connecting SSE for ${modelName}`);
// Subscribe to progress updates via Server-Sent Events
const eventSource = new EventSource(`${serverUrl}/models/progress/${modelName}`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as ModelProgressType;
setProgress(data);
// Close connection if complete or error
if (data.status === 'complete' || data.status === 'error') {
console.log(`[ModelProgress] Download ${data.status} for ${modelName}, closing SSE`);
eventSource.close();
}
} catch (error) {
console.error('Error parsing progress event:', error);
}
};
eventSource.onerror = (error) => {
console.error(`[ModelProgress] SSE error for ${modelName}:`, error);
eventSource.close();
};
return () => {
console.log(`[ModelProgress] Cleanup - closing SSE for ${modelName}`);
eventSource.close();
};
}, [serverUrl, modelName, isDownloading]);
// Don't render if no progress or if complete/error and some time has passed
if (
!progress ||
(progress.status === 'complete' && Date.now() - new Date(progress.timestamp).getTime() > 5000)
) {
return null;
}
const formatBytes = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;
};
const getStatusIcon = () => {
switch (progress.status) {
case 'error':
return <XCircle className="h-4 w-4 text-destructive" />;
case 'downloading':
case 'extracting':
return <Loader2 className="h-4 w-4 animate-spin" />;
default:
return null;
}
};
const getStatusText = () => {
switch (progress.status) {
case 'complete':
return 'Download complete';
case 'error':
return `Error: ${progress.error || 'Unknown error'}`;
case 'downloading':
return progress.filename ? `Downloading ${progress.filename}...` : 'Downloading...';
case 'extracting':
return 'Extracting...';
default:
return 'Processing...';
}
};
return (
<Card className="mb-4">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium flex items-center gap-2">
{getStatusIcon()}
{displayName}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>{getStatusText()}</span>
{progress.total > 0 && (
<span>
{formatBytes(progress.current)} / {formatBytes(progress.total)} (
{progress.progress.toFixed(1)}%)
</span>
)}
</div>
{progress.total > 0 && <Progress value={progress.progress} className="h-2" />}
</div>
</CardContent>
</Card>
);
}
|