mixvideo-v2/apps/desktop/src/components/VideoClassificationProgress...

321 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState, useCallback } from 'react';
import {
Brain, Clock, CheckCircle, XCircle, AlertCircle, Pause, Play, Square,
TrendingUp, BarChart3, Eye, Star, Target
} from 'lucide-react';
import { useVideoClassificationStore } from '../store/videoClassificationStore';
import type { QueueStats, TaskProgress, ClassificationStats } from '../store/videoClassificationStore';
interface VideoClassificationProgressProps {
materialId?: string;
projectId?: string;
autoRefresh?: boolean;
refreshInterval?: number;
}
/**
* AI视频分类进度显示组件
* 遵循前端开发规范的UI设计提供优美的动画效果和用户体验
*/
export const VideoClassificationProgress: React.FC<VideoClassificationProgressProps> = ({
materialId,
projectId,
autoRefresh = true,
refreshInterval = 3000,
}) => {
const {
queueStats,
taskProgress,
refreshQueueStatus,
refreshTaskProgress,
getProjectQueueStatus,
getProjectTaskProgress,
getClassificationStats,
error,
clearError,
} = useVideoClassificationStore();
// Type assertions to satisfy TypeScript
const typedQueueStats: QueueStats | null = queueStats;
const typedTaskProgress: Record<string, TaskProgress> = taskProgress;
const [stats, setStats] = useState<ClassificationStats | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
// 刷新队列状态的方法
const refreshQueueStats = useCallback(async () => {
if (projectId) {
return await getProjectQueueStatus(projectId);
} else {
return await refreshQueueStatus();
}
}, [projectId, getProjectQueueStatus, refreshQueueStatus]);
// 刷新任务进度的方法
const refreshProgress = useCallback(async () => {
if (projectId) {
await getProjectTaskProgress(projectId);
} else {
await refreshTaskProgress();
}
}, [projectId, getProjectTaskProgress, refreshTaskProgress]);
// 自动刷新
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(async () => {
await refreshQueueStats();
await refreshProgress();
if (projectId) {
try {
const classificationStats = await getClassificationStats(projectId);
setStats(classificationStats);
} catch (error) {
console.error('获取分类统计失败:', error);
}
}
}, refreshInterval);
return () => clearInterval(interval);
}, [autoRefresh, refreshInterval, projectId, refreshQueueStats, refreshProgress, getClassificationStats]);
// 初始加载
useEffect(() => {
refreshQueueStats();
refreshProgress();
if (projectId) {
getClassificationStats(projectId).then(setStats).catch(console.error);
}
}, [projectId, refreshQueueStats, refreshProgress, getClassificationStats]);
// 获取状态颜色和图标
const getStatusInfo = (status: string) => {
switch (status) {
case 'Running':
return { color: 'text-green-600 bg-green-50', icon: Play, text: '运行中' };
case 'Paused':
return { color: 'text-yellow-600 bg-yellow-50', icon: Pause, text: '已暂停' };
case 'Stopped':
return { color: 'text-gray-600 bg-gray-50', icon: Square, text: '已停止' };
default:
return { color: 'text-gray-600 bg-gray-50', icon: Square, text: '未知' };
}
};
// 获取任务状态信息
const getTaskStatusInfo = (status: string) => {
switch (status) {
case 'Pending':
return { color: 'text-blue-600', icon: Clock, text: '等待中' };
case 'Uploading':
return { color: 'text-purple-600', icon: TrendingUp, text: '上传中' };
case 'Analyzing':
return { color: 'text-indigo-600', icon: Brain, text: '分析中' };
case 'Completed':
return { color: 'text-green-600', icon: CheckCircle, text: '已完成' };
case 'Failed':
return { color: 'text-red-600', icon: XCircle, text: '失败' };
case 'Cancelled':
return { color: 'text-gray-600', icon: AlertCircle, text: '已取消' };
default:
return { color: 'text-gray-600', icon: Clock, text: '未知' };
}
};
// 计算进度百分比
const getOverallProgress = () => {
if (!typedQueueStats || typedQueueStats.total_tasks === 0) return 0;
return Math.round(((typedQueueStats.completed_tasks + typedQueueStats.failed_tasks) / typedQueueStats.total_tasks) * 100);
};
// 过滤相关任务
const relevantTasks = materialId
? Object.values(typedTaskProgress).filter(task => task.task_id.includes(materialId))
: Object.values(typedTaskProgress);
if (!typedQueueStats && !relevantTasks.length && !stats) {
return null;
}
const statusInfo = typedQueueStats ? getStatusInfo(typedQueueStats.status) : null;
const overallProgress = getOverallProgress()
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
{/* 错误提示 */}
{error && (
<div className="bg-red-50 border-l-4 border-red-400 p-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<XCircle className="w-4 h-4 text-red-400 mr-2" />
<span className="text-sm text-red-700">{error}</span>
</div>
<button
onClick={clearError}
className="text-red-400 hover:text-red-600"
>
<XCircle className="w-4 h-4" />
</button>
</div>
</div>
)}
{/* 主要状态显示 */}
<div className="p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2">
<Brain className="w-5 h-5 text-purple-600" />
<h3 className="text-lg font-medium text-gray-900">AI视频分类</h3>
</div>
{statusInfo && (
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusInfo.color}`}>
<statusInfo.icon className="w-3 h-3 mr-1" />
{statusInfo.text}
</span>
)}
</div>
{/* 队列控制按钮 */}
{typedQueueStats && (
<div className="flex items-center space-x-2">
<div className="text-xs text-gray-500 ml-2">
: {typedQueueStats.status}
</div>
</div>
)}
</div>
{/* 整体进度条 */}
{typedQueueStats && typedQueueStats.total_tasks > 0 && (
<div className="mb-4">
<div className="flex items-center justify-between text-sm text-gray-600 mb-2">
<span></span>
<span>{overallProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full transition-all duration-300 ease-out"
style={{ width: `${overallProgress}%` }}
/>
</div>
</div>
)}
{/* 统计信息网格 */}
{typedQueueStats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="text-center p-3 bg-blue-50 rounded-lg">
<div className="text-2xl font-bold text-blue-600">{typedQueueStats.pending_tasks}</div>
<div className="text-xs text-blue-600"></div>
</div>
<div className="text-center p-3 bg-purple-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">{typedQueueStats.processing_tasks}</div>
<div className="text-xs text-purple-600"></div>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<div className="text-2xl font-bold text-green-600">{typedQueueStats.completed_tasks}</div>
<div className="text-xs text-green-600"></div>
</div>
<div className="text-center p-3 bg-red-50 rounded-lg">
<div className="text-2xl font-bold text-red-600">{typedQueueStats.failed_tasks}</div>
<div className="text-xs text-red-600"></div>
</div>
</div>
)}
{/* 处理速率 */}
{typedQueueStats && typedQueueStats.processing_rate > 0 && (
<div className="flex items-center justify-center text-sm text-gray-600 mb-4">
<TrendingUp className="w-4 h-4 mr-1" />
<span>: {typedQueueStats.processing_rate.toFixed(1)} /</span>
</div>
)}
{/* 展开/收起详细信息 */}
{relevantTasks.length > 0 && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full text-sm text-blue-600 hover:text-blue-800 py-2 border-t border-gray-100"
>
{isExpanded ? '收起详细信息' : `查看详细信息 (${relevantTasks.length} 个任务)`}
</button>
)}
</div>
{/* 详细任务列表 */}
{isExpanded && relevantTasks.length > 0 && (
<div className="border-t border-gray-100 bg-gray-50">
<div className="p-4 space-y-3 max-h-64 overflow-y-auto">
{relevantTasks.map((task) => {
const taskStatusInfo = getTaskStatusInfo(task.status);
return (
<div key={task.task_id} className="bg-white rounded-lg p-3 shadow-sm">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<taskStatusInfo.icon className={`w-4 h-4 ${taskStatusInfo.color}`} />
<span className="text-sm font-medium text-gray-900">
#{task.task_id.slice(-8)}
</span>
</div>
<span className={`text-xs ${taskStatusInfo.color}`}>
{taskStatusInfo.text}
</span>
</div>
<div className="text-xs text-gray-600 mb-2">{task.current_step}</div>
{task.progress_percentage > 0 && (
<div className="w-full bg-gray-200 rounded-full h-1.5">
<div
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
style={{ width: `${task.progress_percentage}%` }}
/>
</div>
)}
{task.error_message && (
<div className="mt-2 text-xs text-red-600 bg-red-50 p-2 rounded">
{task.error_message}
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* 分类统计 */}
{stats && (
<div className="border-t border-gray-100 bg-gradient-to-r from-blue-50 to-purple-50 p-4">
<div className="flex items-center space-x-2 mb-3">
<BarChart3 className="w-4 h-4 text-blue-600" />
<span className="text-sm font-medium text-gray-900"></span>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-xs">
<div className="flex items-center space-x-2">
<Eye className="w-3 h-3 text-blue-500" />
<span className="text-gray-600">: {stats.total_classifications}</span>
</div>
<div className="flex items-center space-x-2">
<Target className="w-3 h-3 text-green-500" />
<span className="text-gray-600">: {(stats.average_confidence * 100).toFixed(1)}%</span>
</div>
<div className="flex items-center space-x-2">
<Star className="w-3 h-3 text-yellow-500" />
<span className="text-gray-600">: {(stats.average_quality_score * 100).toFixed(1)}%</span>
</div>
</div>
</div>
)}
</div>
);
};