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 ? (
/* 🔥 极度直观的全新多维进度看板 */
{/* 大数字百分比 */}
{progressPercent}
%
正在执行第 {curStep} 步 / 共 {totalSteps} 步
{/* 真实横向流光进度条 */}
{/* 进度条两端时间指示 */}
已耗时: {elapsed} 秒
{estimatedRemaining && (
预计还需: {estimatedRemaining}
)}
{/* 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}
))}
{(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 }))}
/>
);
}