"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { TestTube, Loader2, Download, CheckCircle2, AlertCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { toast } from "sonner"; const API_PREFIX = "/api/testcase-generator"; interface GenerateRequest { title: string; englishName: string; description: string; standardCode: string; serviceLevel: string; testCaseCount: number; } interface TaskStatusResponse { task_id: string; status: "pending" | "running" | "completed" | "failed"; progress?: number; title?: string; english_name?: string; test_case_count?: number; generated_cases?: number; error_message?: string; currentMessage?: string; } interface CompletedResult { title?: string; english_name?: string; test_case_count?: number; file_size?: string; } export default function TestcaseGeneratorPage() { const [problemTitle, setProblemTitle] = useState(""); const [problemSlug, setProblemSlug] = useState(""); const [problemDescription, setProblemDescription] = useState(""); const [stdProgram, setStdProgram] = useState(""); const [serviceLevel, setServiceLevel] = useState<"standard" | "pro">("standard"); const [testCaseCount, setTestCaseCount] = useState(10); const [isGenerating, setIsGenerating] = useState(false); const [progressOpen, setProgressOpen] = useState(false); const [progressTitle, setProgressTitle] = useState("正在生成测试数据"); const [progressMessage, setProgressMessage] = useState("请稍候..."); const [progressPercent, setProgressPercent] = useState(0); const [progressTaskId, setProgressTaskId] = useState(""); const [progressGenerated, setProgressGenerated] = useState(0); const [progressTotal, setProgressTotal] = useState(0); const [progressRunningTime, setProgressRunningTime] = useState(0); const pollTimerRef = useRef | null>(null); const startTimeRef = useRef(0); const [resultModalOpen, setResultModalOpen] = useState(false); const [resultData, setResultData] = useState(null); const [errorBlock, setErrorBlock] = useState<{ title: string; message: string } | null>(null); const slugPattern = /^[a-z][a-z0-9_]*$/; const showProgress = useCallback(() => { setProgressOpen(true); setProgressPercent(0); setProgressGenerated(0); setProgressTotal(0); setProgressRunningTime(0); setProgressTaskId(""); startTimeRef.current = Date.now(); }, []); const hideProgress = useCallback(() => { setProgressOpen(false); if (pollTimerRef.current) { clearTimeout(pollTimerRef.current); pollTimerRef.current = null; } }, []); const updateProgress = useCallback( (opts: { title?: string; message?: string; progress?: number; taskId?: string; generatedCount?: number; totalCount?: number; }) => { if (opts.title !== undefined) setProgressTitle(opts.title); if (opts.message !== undefined) setProgressMessage(opts.message); if (opts.progress !== undefined) setProgressPercent(Math.min(100, Math.max(0, opts.progress))); if (opts.taskId !== undefined) setProgressTaskId(opts.taskId); if (opts.generatedCount !== undefined) setProgressGenerated(opts.generatedCount); if (opts.totalCount !== undefined) setProgressTotal(opts.totalCount); }, [] ); const pollTaskStatus = useCallback( async (taskId: string, problemTitle: string, estimatedTimeSec: number) => { const tick = () => { setProgressRunningTime(Math.floor((Date.now() - startTimeRef.current) / 1000)); }; const interval = setInterval(tick, 1000); const check = async () => { try { const res = await fetch(`${API_PREFIX}/task_status/${taskId}`); const data: TaskStatusResponse = await res.json(); updateProgress({ progress: data.progress ?? 0, generatedCount: data.generated_cases ?? 0, totalCount: data.test_case_count ?? 0, message: data.currentMessage || "正在处理...", }); if (data.status === "completed") { clearInterval(interval); hideProgress(); setIsGenerating(false); setResultData({ title: data.title ?? problemTitle, english_name: data.english_name, test_case_count: data.test_case_count, file_size: "计算中...", task_id: taskId, }); setResultModalOpen(true); setErrorBlock(null); toast.success("测试数据生成完成!"); return; } if (data.status === "failed") { clearInterval(interval); hideProgress(); setIsGenerating(false); setErrorBlock({ title: "测试数据生成失败", message: data.error_message || "未知错误", }); toast.error("测试数据生成失败"); return; } pollTimerRef.current = setTimeout(check, 2000); } catch (err) { clearInterval(interval); hideProgress(); setIsGenerating(false); const msg = err instanceof Error ? err.message : String(err); toast.error("获取任务状态失败: " + msg); } }; await check(); }, [updateProgress, hideProgress] ); const handleGenerate = useCallback(async () => { const title = problemTitle.trim(); const slug = problemSlug.trim(); const description = problemDescription.trim(); const code = stdProgram.trim(); if (!title || !slug || !description || !code) { toast.error("请填写所有必填字段"); return; } if (!slugPattern.test(slug)) { toast.error("题目英文名只能包含小写字母、数字和下划线,且必须以字母开头"); return; } const count = Math.min(50, Math.max(1, testCaseCount)); if (count !== testCaseCount) setTestCaseCount(count); setIsGenerating(true); setErrorBlock(null); showProgress(); updateProgress({ title: "正在生成测试数据", message: "正在初始化...", taskId: "" }); try { const body: GenerateRequest = { title, englishName: slug, description, standardCode: code, serviceLevel, testCaseCount: count, }; const res = await fetch(`${API_PREFIX}/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const result = await res.json(); if (!res.ok) { throw new Error(result.error || result.message || "生成请求失败"); } if (result.success && result.task_id) { const estimatedTime = result.estimated_time ?? 60; updateProgress({ taskId: result.task_id, title: `正在生成「${title}」的测试数据`, message: `预计需要 ${Math.ceil(estimatedTime / 60)} 分钟,请耐心等待...`, totalCount: count, }); await pollTaskStatus(result.task_id, title, estimatedTime); } else { throw new Error(result.error || "未返回任务 ID"); } } catch (err) { hideProgress(); setIsGenerating(false); const msg = err instanceof Error ? err.message : String(err); toast.error("生成失败: " + msg); } }, [ problemTitle, problemSlug, problemDescription, stdProgram, serviceLevel, testCaseCount, showProgress, updateProgress, hideProgress, pollTaskStatus, ]); const handleCancelGeneration = useCallback(() => { hideProgress(); setIsGenerating(false); toast.info("生成已取消"); }, [hideProgress]); const handleDownload = useCallback((taskId: string) => { if (!taskId) return; const url = `${API_PREFIX}/download/${taskId}`; window.open(url, "_blank"); toast.success("开始下载测试数据"); }, []); const handleCloseResultModal = useCallback(() => { setResultModalOpen(false); setResultData(null); }, []); useEffect(() => { return () => { if (pollTimerRef.current) clearTimeout(pollTimerRef.current); }; }, []); const runningTimeStr = progressRunningTime >= 60 ? `${Math.floor(progressRunningTime / 60)}分${progressRunningTime % 60}秒` : `${progressRunningTime}秒`; return (

测试数据生成

为信息学奥林匹克竞赛生成高质量测试数据

{/* 题目信息 */} 题目信息
setProblemTitle(e.target.value)} />

简短描述题目名称

setProblemSlug(e.target.value.toLowerCase())} />

用作文件名前缀,只能包含小写字母、数字和下划线,必须以字母开头