import { useMemo } from "react"; import { trpc } from "@/lib/trpc"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ScrollArea } from "@/components/ui/scroll-area"; import { toast } from "sonner"; import { AlertTriangle, BellRing, CheckCircle2, ClipboardList, Loader2, RefreshCcw } from "lucide-react"; function formatTaskStatus(status: string) { switch (status) { case "running": return "执行中"; case "succeeded": return "已完成"; case "failed": return "失败"; default: return "排队中"; } } function formatNotificationState(isRead: number | boolean | null | undefined) { return isRead ? "已读" : "未读"; } function formatStructuredValue(value: unknown) { if (!value) return ""; if (typeof value === "string") return value; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } function formatTaskTiming(task: { createdAt: string | Date; startedAt?: string | Date | null; completedAt?: string | Date | null; }) { const createdAt = new Date(task.createdAt).getTime(); const startedAt = task.startedAt ? new Date(task.startedAt).getTime() : null; const completedAt = task.completedAt ? new Date(task.completedAt).getTime() : null; const durationMs = (completedAt ?? Date.now()) - (startedAt ?? createdAt); const seconds = Math.max(0, Math.round(durationMs / 1000)); if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const rest = seconds % 60; return `${minutes}m ${rest.toString().padStart(2, "0")}s`; } export default function Logs() { const utils = trpc.useUtils(); const taskListQuery = trpc.task.list.useQuery( { limit: 50 }, { retry: 3, retryDelay: (attempt) => Math.min(1_000 * 2 ** attempt, 8_000), placeholderData: (previous) => previous, refetchInterval: (query) => { const hasActiveTask = (query.state.data ?? []).some((task) => task.status === "queued" || task.status === "running"); return hasActiveTask ? 3_000 : 10_000; }, }, ); const notificationQuery = trpc.notification.list.useQuery({ limit: 50 }); const retryMutation = trpc.task.retry.useMutation({ onSuccess: () => { utils.task.list.invalidate(); toast.success("任务已重新排队"); }, onError: (error) => toast.error(`任务重试失败: ${error.message}`), }); const activeTaskCount = useMemo( () => (taskListQuery.data ?? []).filter((task) => task.status === "queued" || task.status === "running").length, [taskListQuery.data], ); const failedTaskCount = useMemo( () => (taskListQuery.data ?? []).filter((task) => task.status === "failed").length, [taskListQuery.data], ); const unreadNotificationCount = useMemo( () => (notificationQuery.data ?? []).filter((item) => !item.isRead).length, [notificationQuery.data], ); if (taskListQuery.isLoading && notificationQuery.isLoading) { return (
查看后台任务、归档失败原因和通知记录。录制结束失败、训练计划生成失败等信息会保留在这里。
{task.message || formatTaskStatus(task.status)}
{task.error ? (
{formatStructuredValue(task.result)}
) : null}
{item.message || "无附加内容"}