import React, { useState, useEffect } from 'react'; import { ArrowLeft, Download, RotateCcw, ArrowUpToLine, StopCircle, XCircle, Copy, Check, Code, Cpu, Clock, Layers, ShieldAlert, Sparkles, Activity } from 'lucide-react'; import { fetchJobDetail, prioritizeJob, cancelJob, fetchComfyHistory } from '../api'; import { formatDateTime, formatBytes, thumbUrl, localAsset, h3FileUrl, getH3Base, getJobKind } from '../utils'; import { useToast } from './Toast'; import Modal from './Modal'; export default function TaskDetail({ taskId, onBack, onRerun }) { const { addToast } = useToast(); const [job, setJob] = useState(null); const [loading, setLoading] = useState(true); const [copiedPrompt, setCopiedPrompt] = useState(false); const [comfyHistory, setComfyHistory] = useState(null); const [loadingHistory, setLoadingHistory] = useState(false); const [showHistory, setShowHistory] = useState(false); const [showRawReq, setShowRawReq] = useState(false); // 模态弹窗状态 const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', content: '', isDanger: false, onConfirm: null }); const loadDetail = async () => { try { const data = await fetchJobDetail(taskId); setJob(data); } catch (e) { console.error(e); } finally { setLoading(false); } }; useEffect(() => { loadDetail(); // 轮询:排队或运行中每 2.5 秒刷新一次 const timer = setInterval(() => { if (job && (job.status === 'queued' || job.status === 'running')) { loadDetail(); } }, 2500); return () => clearInterval(timer); }, [taskId, job?.status]); if (loading && !job) { return (
正在加载任务详情…
); } if (!job) { return (

未找到该任务或已被彻底删除

); } const req = job.req || {}; const isDone = job.status === 'done'; const isRunning = job.status === 'running'; const isQueued = job.status === 'queued'; const isFailed = job.status === 'failed'; const isCancelled = job.status === 'cancelled'; // 进度精细计算 const curStep = job.step || 0; const totalSteps = job.total_steps || req.steps || 20; const progressPercent = totalSteps > 0 ? Math.min(100, Math.max(curStep > 0 ? 5 : 2, Math.round((curStep / totalSteps) * 100))) : 0; // 预估剩余时间 (秒) const elapsed = job.elapsed_seconds || 0; let estimatedRemaining = null; if (curStep > 0 && totalSteps > curStep) { const secPerStep = elapsed / curStep; const remainSec = Math.round(secPerStep * (totalSteps - curStep)); estimatedRemaining = remainSec > 60 ? `约 ${Math.round(remainSec / 60)} 分钟` : `约 ${remainSec} 秒`; } const videoSrc = isDone ? h3FileUrl(`${getH3Base()}/v1/videos/${job.id}/file`) : null; const handleCopyPrompt = () => { if (!req.prompt) return; navigator.clipboard.writeText(req.prompt); setCopiedPrompt(true); addToast('已复制 Prompt 提示词到剪贴板', 'success'); setTimeout(() => setCopiedPrompt(false), 2000); }; const handlePrioritize = async () => { try { const res = await prioritizeJob(job.id); addToast(`已成功插队至第 ${res.queue_position} 位`, 'success'); loadDetail(); } catch (e) { addToast('插队失败: ' + e.message, 'error'); } }; const handleCancel = (running) => { setConfirmModal({ isOpen: true, title: running ? '停止正在运行的任务' : '取消排队任务', content: running ? '确定停止当前正在运行的任务?\n这会硬重启后台 Raylight 服务以释放 GPU 显存(约耗时 15-20 秒),重启完成前将暂时无法提交新任务。' : '确定取消这条排队中的任务?', isDanger: running, onConfirm: async () => { setConfirmModal((prev) => ({ ...prev, isOpen: false })); try { await cancelJob(job.id); addToast(running ? '已发出停止与 GPU 重置指令' : '已取消任务', 'info'); loadDetail(); } catch (e) { addToast('操作失败: ' + e.message, 'error'); } }, }); }; const handleToggleHistory = async () => { if (!showHistory && !comfyHistory) { setLoadingHistory(true); try { const hist = await fetchComfyHistory(job.id); setComfyHistory(hist); } catch (e) { addToast('获取 ComfyUI History 失败: ' + e.message, 'error'); } finally { setLoadingHistory(false); } } setShowHistory(!showHistory); }; return (
{/* 顶栏导航与快捷动作 */}

#{job.id}

{/* 状态标识 */} {isDone && 已完成} {isRunning && ( 正在采样 · {progressPercent}% )} {isQueued && ( 排队中 {job.queue_position ? `(第 ${job.queue_position} 位)` : ''} )} {isFailed && 执行失败} {isCancelled && 已取消}
创建于 {formatDateTime(job.created_at)}
{/* 顶部操作条 */}
{isQueued && ( <> )} {isRunning && ( )}
{/* 核心成果展示区 / 实时可视化进度看板 */}
{isDone && videoSrc ? (
) : isRunning ? ( /* 🔥 极度直观的全新多维进度看板 */
GPU 扩散采样中 · 实时推演
{/* 大数字百分比 */}
{progressPercent} %

正在执行第 {curStep} 步 / 共 {totalSteps} 步

{/* 真实横向流光进度条 */}
{/* 流光扫光动画 */}
{/* 进度条两端时间指示 */}
已耗时: {elapsed} 秒 {estimatedRemaining && ( 预计还需: {estimatedRemaining} )}
{/* 4 阶段步骤状态指示链 */}
✓
1. 排队调度
✓
2. 模型装载
3
3. 扩散计算
4
4. 压制出片
) : isQueued ? (

正在等待空闲的 {job.partition || 'GPU'} 后端…

当前排队第 {job.queue_position || 1} 位,空闲节点就绪后将自动调度启动

) : (

{isFailed ? '生成失败' : '任务已取消'}

{job.error || '无详细错误日志'}
)}
{/* 仪表盘指标卡片网格 */}
生成耗时 {job.started_at && job.finished_at ? `${(job.finished_at - job.started_at).toFixed(1)} 秒` : isRunning ? `${job.elapsed_seconds || 0} 秒 (进行中)` : '—'}
峰值显存 {job.peak_vram_gb ? `${job.peak_vram_gb} GB` : '—'}
分配后端卡 {job.backend || '自动弹性调度'}
采样规格 {req.turbo === false ? '常规' : 'Turbo'} · {req.steps ?? '?'}步 · seed={req.seed ?? 0}
{/* Prompt 提示词卡片 */}
Prompt 提示词

{req.prompt || '(无提示词)'}

{req.negative_prompt && (
负面提示词: {req.negative_prompt}
)}
{/* 输入素材回显 (首尾帧 / 参考媒体) */} {(req.first_frame_url || req.last_frame_url || (req.reference_image_urls || []).length > 0 || (req.reference_video_urls || []).length > 0 || (req.reference_audio_urls || []).length > 0) && (
输入素材与参考源
{req.first_frame_url && (
首帧
首帧
)} {req.last_frame_url && (
尾帧
尾帧
)} {(req.reference_image_urls || []).map((u, i) => (
{`参考图${i+1}`}
参考图 {i+1}
))} {(req.reference_video_urls || []).map((u, i) => (
))}
)} {/* 底部调试折叠抽屉 */}
{/* 原始请求 JSON */}
{showRawReq && (
              {JSON.stringify(req, null, 2)}
            
)}
{/* ComfyUI History */} {job.comfy_prompt_id && (
{showHistory && (
                {loadingHistory ? '正在拉取 ComfyUI History…' : comfyHistory ? JSON.stringify(comfyHistory, null, 2) : '无可用数据'}
              
)}
)}
{/* 模态弹窗 */} setConfirmModal((prev) => ({ ...prev, isOpen: false }))} />
); }