import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from "motion/react"; import { ChevronDown, ChevronUp, Clock3, Coins, ListTodo, LoaderCircle, XCircle } from "lucide-react"; import { useEffect, useState } from "react"; import { formatCredits } from "@/constant/credits"; import { aceternityMotion } from "@/lib/aceternity-motion"; import { formatTaskKind, generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "@/lib/generation-task-display"; import { canvasThemes } from "@/lib/canvas-theme"; import type { GenerationTask } from "@/services/api/task-center"; import { useThemeStore } from "@/stores/use-theme-store"; import { useUserStore } from "@/stores/use-user-store"; // 顶栏是绝对定位浮层,面板必须按调用方传入的 topInset 避让;专注模式隐藏顶栏时传小间距。 export function CanvasActiveTaskPanel({ tasks, align = "right", topInset = "var(--canvas-topbar-offset)", onCancelTask }: { tasks: GenerationTask[]; align?: "left" | "right"; topInset?: string; onCancelTask?: (task: GenerationTask) => void }) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); const reducedMotion = useReducedMotion(); const [now, setNow] = useState(() => Date.now()); const [open, setOpen] = useState(false); const [expandedTaskId, setExpandedTaskId] = useState(null); useEffect(() => { if (!tasks.length) return; const timer = window.setInterval(() => setNow(Date.now()), 1_000); return () => window.clearInterval(timer); }, [tasks.length]); useEffect(() => { if (expandedTaskId && !tasks.some((task) => task.id === expandedTaskId)) setExpandedTaskId(null); }, [expandedTaskId, tasks]); if (!tasks.length) return null; const motionTransition = reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel; return ( setOpen((value) => !value)} aria-expanded={open} aria-controls="canvas-active-task-list" > 生成任务 当前画布 · {tasks.length} 个进行中 {open ? : } {open ? ( {tasks.map((task) => ( setExpandedTaskId((current) => (current === task.id ? null : task.id))} onCancelTask={onCancelTask} reducedMotion={Boolean(reducedMotion)} creditsEnabled={creditsEnabled} /> ))} ) : null} ); } function ActiveTaskCard({ task, now, theme, expanded, onToggle, onCancelTask, reducedMotion, creditsEnabled, }: { task: GenerationTask; now: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; expanded: boolean; onToggle: () => void; onCancelTask?: (task: GenerationTask) => void; reducedMotion: boolean; creditsEnabled: boolean; }) { const showsProgress = generationTaskShowsProgress(task); const progress = showsProgress && typeof task.progress === "number" ? Math.max(0, Math.min(100, Math.round(task.progress))) : showsProgress && task.status === "queued" ? 0 : undefined; const startedAt = task.startedAt || task.createdAt; const elapsedMs = Math.max(0, now - parseTime(startedAt)); const durationLabel = `${task.status === "queued" ? "已等待" : "已运行"} ${formatDuration(elapsedMs)}`; const billingLabel = task.billing ? `冻结 ${formatCredits(task.billing.amountMicrocredits)} 积分` : "未计费"; const statusTone = task.status === "running" ? theme.accent.primary : theme.node.muted; const transition = reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel; return ( {expanded ? (
当前阶段 {generationTaskStageLabel(task)}
{onCancelTask && (task.status === "queued" || task.status === "running") ? ( ) : null}
) : null}
); } function parseTime(value?: string) { if (!value) return Date.now(); const time = new Date(value).getTime(); return Number.isFinite(time) ? time : Date.now(); } function formatDuration(value: number) { const totalSeconds = Math.floor(value / 1_000); const hours = Math.floor(totalSeconds / 3_600); const minutes = Math.floor((totalSeconds % 3_600) / 60); const seconds = totalSeconds % 60; return hours ? `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}` : `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; }