| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- 'use client';
- import { useState, useEffect } from 'react';
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
- import { Button } from '@/components/ui/button';
- import { Progress } from '@/components/ui/progress';
- import { Badge } from '@/components/ui/badge';
- import { apiClient, type JobInfo } from '@/lib/api';
- import {
- Loader2,
- CheckCircle2,
- XCircle,
- Clock,
- AlertCircle,
- RefreshCw,
- Eye,
- Trash2
- } from 'lucide-react';
- import { cn } from '@/lib/utils';
- interface ModelConversionProgressProps {
- requestId: string;
- modelName: string;
- quantizationType: string;
- onComplete?: () => void;
- onCancel?: () => void;
- }
- interface ConversionStatus {
- status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled';
- progress: number;
- message: string;
- error?: string;
- createdAt: string;
- updatedAt: string;
- outputPath?: string;
- }
- export function ModelConversionProgress({
- requestId,
- modelName,
- quantizationType,
- onComplete,
- onCancel
- }: ModelConversionProgressProps) {
- const [conversionStatus, setConversionStatus] = useState<ConversionStatus | null>(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
- const [polling, setPolling] = useState(true);
- useEffect(() => {
- if (!polling) return;
- const checkStatus = async () => {
- try {
- // Try to get job status from queue
- const queueStatus = await apiClient.getQueueStatus();
- const job = queueStatus.jobs.find(j => j.request_id === requestId || j.id === requestId);
- if (job) {
- const status: ConversionStatus = {
- status: job.status as any,
- progress: job.progress || 0,
- message: job.message || 'Processing...',
- error: job.error,
- createdAt: job.created_at || new Date().toISOString(),
- updatedAt: job.updated_at || new Date().toISOString(),
- };
- setConversionStatus(status);
- setError(null);
- // Stop polling if completed or failed
- if (job.status === 'completed') {
- setPolling(false);
- onComplete?.();
- } else if (job.status === 'failed' || job.status === 'cancelled') {
- setPolling(false);
- setError(job.error || 'Conversion failed');
- }
- } else {
- // Job not found in queue, might be completed or failed
- if (conversionStatus?.status !== 'completed') {
- setPolling(false);
- setError('Conversion job not found in queue');
- }
- }
- } catch (err) {
- console.error('Failed to check conversion status:', err);
- setError('Failed to check status');
- } finally {
- setLoading(false);
- }
- };
- checkStatus();
- // Poll every 2 seconds when active
- const interval = polling ? setInterval(checkStatus, 2000) : null;
- return () => {
- if (interval) clearInterval(interval);
- };
- }, [requestId, polling, onComplete, conversionStatus?.status]);
- const getStatusIcon = () => {
- if (loading) return <Loader2 className="h-5 w-5 animate-spin" />;
- switch (conversionStatus?.status) {
- case 'completed':
- return <CheckCircle2 className="h-5 w-5 text-green-500" />;
- case 'failed':
- return <XCircle className="h-5 w-5 text-red-500" />;
- case 'processing':
- return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
- case 'cancelled':
- return <XCircle className="h-5 w-5 text-gray-500" />;
- case 'pending':
- return <Clock className="h-5 w-5 text-yellow-500" />;
- default:
- return <AlertCircle className="h-5 w-5 text-gray-500" />;
- }
- };
- const getStatusColor = () => {
- switch (conversionStatus?.status) {
- case 'completed':
- return 'text-green-600 dark:text-green-400';
- case 'failed':
- return 'text-red-600 dark:text-red-400';
- case 'processing':
- return 'text-blue-600 dark:text-blue-400';
- case 'cancelled':
- return 'text-gray-600 dark:text-gray-400';
- case 'pending':
- return 'text-yellow-600 dark:text-yellow-400';
- default:
- return 'text-gray-600 dark:text-gray-400';
- }
- };
- const getStatusBadgeVariant = () => {
- switch (conversionStatus?.status) {
- case 'completed':
- return 'default';
- case 'failed':
- return 'destructive';
- case 'processing':
- return 'secondary';
- case 'cancelled':
- return 'outline';
- case 'pending':
- return 'outline';
- default:
- return 'outline';
- }
- };
- if (error && !conversionStatus) {
- return (
- <Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/20">
- <CardContent className="p-6">
- <div className="flex items-center gap-3">
- <XCircle className="h-5 w-5 text-red-500" />
- <div>
- <h3 className="font-semibold text-red-900 dark:text-red-100">Conversion Error</h3>
- <p className="text-sm text-red-700 dark:text-red-300">{error}</p>
- </div>
- </div>
- </CardContent>
- </Card>
- );
- }
- return (
- <Card>
- <CardHeader>
- <div className="flex items-center justify-between">
- <div>
- <CardTitle className="flex items-center gap-2">
- {getStatusIcon()}
- Model Conversion
- </CardTitle>
- <CardDescription>
- Converting {modelName} to {quantizationType}
- </CardDescription>
- </div>
- <Badge variant={getStatusBadgeVariant()}>
- {conversionStatus?.status || 'Unknown'}
- </Badge>
- </div>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="space-y-2">
- <div className="flex items-center justify-between text-sm">
- <span className={cn("font-medium", getStatusColor())}>
- {conversionStatus?.message || 'Initializing...'}
- </span>
- <span className="text-muted-foreground">
- {conversionStatus ? Math.round(conversionStatus.progress * 100) : 0}%
- </span>
- </div>
- <Progress
- value={conversionStatus?.progress || 0}
- className="h-2"
- />
- </div>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div>
- <span className="text-muted-foreground">Model:</span>
- <p className="font-medium">{modelName}</p>
- </div>
- <div>
- <span className="text-muted-foreground">Quantization:</span>
- <p className="font-medium">{quantizationType}</p>
- </div>
- {conversionStatus?.createdAt && (
- <div>
- <span className="text-muted-foreground">Started:</span>
- <p className="font-medium">
- {new Date(conversionStatus.createdAt).toLocaleTimeString()}
- </p>
- </div>
- )}
- {conversionStatus?.updatedAt && (
- <div>
- <span className="text-muted-foreground">Updated:</span>
- <p className="font-medium">
- {new Date(conversionStatus.updatedAt).toLocaleTimeString()}
- </p>
- </div>
- )}
- </div>
- {conversionStatus?.error && (
- <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
- <p className="text-sm text-red-700 dark:text-red-300">
- <strong>Error:</strong> {conversionStatus.error}
- </p>
- </div>
- )}
- {conversionStatus?.status === 'completed' && (
- <div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
- <p className="text-sm text-green-700 dark:text-green-300">
- <strong>Success!</strong> Model conversion completed successfully.
- </p>
- {conversionStatus.outputPath && (
- <p className="text-xs text-green-600 dark:text-green-400 mt-1">
- Output: {conversionStatus.outputPath}
- </p>
- )}
- </div>
- )}
- <div className="flex gap-2 pt-2">
- {polling && (
- <Button
- variant="outline"
- size="sm"
- onClick={() => setPolling(false)}
- >
- <Eye className="h-4 w-4 mr-2" />
- Stop Watching
- </Button>
- )}
- {!polling && conversionStatus?.status === 'processing' && (
- <Button
- variant="outline"
- size="sm"
- onClick={() => setPolling(true)}
- >
- <RefreshCw className="h-4 w-4 mr-2" />
- Resume Watching
- </Button>
- )}
- {(conversionStatus?.status === 'failed' || conversionStatus?.status === 'cancelled') && (
- <Button
- variant="outline"
- size="sm"
- onClick={onCancel}
- >
- <Trash2 className="h-4 w-4 mr-2" />
- Close
- </Button>
- )}
- </div>
- </CardContent>
- </Card>
- );
- }
|