/** * Exam Detail Page - with real-time parsing progress via SSE */ import React, { useState, useEffect, useRef } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { examAPI, questionAPI } from '../api/client' import ParsingProgress from '../components/ParsingProgress' import { ArrowLeft, Upload, Play, Loader, FileText, AlertCircle, RefreshCw, ArrowRight } from 'lucide-react' import toast from 'react-hot-toast' import { getStatusColor, getStatusText, formatDate, calculateProgress, isValidFileType, getQuestionTypeText } from '../utils/helpers' export const ExamDetail = () => { const { examId } = useParams() const navigate = useNavigate() const [exam, setExam] = useState(null) const [loading, setLoading] = useState(true) const [uploading, setUploading] = useState(false) const [showUploadModal, setShowUploadModal] = useState(false) const [uploadFile, setUploadFile] = useState(null) const [progress, setProgress] = useState(null) const eventSourceRef = useRef(null) useEffect(() => { loadExamDetail() // Cleanup on unmount return () => { if (eventSourceRef.current) { eventSourceRef.current.close() eventSourceRef.current = null } } }, [examId]) const loadExamDetail = async () => { try { const examRes = await examAPI.getDetail(examId) setExam(examRes.data) // Connect to SSE if exam is processing if (examRes.data.status === 'processing') { connectSSE() } } catch (error) { console.error('Failed to load exam:', error) toast.error('加载题库失败') } finally { setLoading(false) } } const connectSSE = () => { // Close existing connection if any if (eventSourceRef.current) { eventSourceRef.current.close() } console.log('[SSE] Connecting to progress stream for exam', examId) const token = localStorage.getItem('token') const url = `/api/exams/${examId}/progress?token=${encodeURIComponent(token)}` const eventSource = new EventSource(url) eventSourceRef.current = eventSource eventSource.onmessage = (event) => { try { const progressData = JSON.parse(event.data) console.log('[SSE] Progress update:', progressData) setProgress(progressData) // Update exam status if completed or failed if (progressData.status === 'completed') { toast.success(progressData.message) setExam(prev => ({ ...prev, status: 'ready' })) loadExamDetail() // Reload to get updated questions eventSource.close() eventSourceRef.current = null } else if (progressData.status === 'failed') { toast.error(progressData.message) setExam(prev => ({ ...prev, status: 'failed' })) eventSource.close() eventSourceRef.current = null } } catch (error) { console.error('[SSE] Failed to parse progress data:', error) } } eventSource.onerror = (error) => { console.error('[SSE] Connection error:', error) eventSource.close() eventSourceRef.current = null } eventSource.onopen = () => { console.log('[SSE] Connection established') } } const handleAppendDocument = async (e) => { e.preventDefault() if (!uploadFile) { toast.error('请选择文件') return } if (!isValidFileType(uploadFile.name)) { toast.error('不支持的文件类型') return } setUploading(true) try { await examAPI.appendDocument(examId, uploadFile) toast.success('文档上传成功,正在解析并去重...') setShowUploadModal(false) setUploadFile(null) setExam(prev => ({ ...prev, status: 'processing' })) // Connect to SSE for real-time progress connectSSE() } catch (error) { console.error('Failed to append document:', error) toast.error('文档上传失败') } finally { setUploading(false) } } const handleStartQuiz = () => { if (exam.current_index >= exam.total_questions) { if (window.confirm('已经完成所有题目,是否从头开始?')) { navigate(`/quiz/${examId}?reset=true`) } } else { navigate(`/quiz/${examId}`) } } if (loading) { return (
题库不存在
题目总数
{exam.total_questions}
已完成
{exam.current_index}
剩余
{Math.max(0, exam.total_questions - exam.current_index)}
完成度
{isProcessing ? progress : quizProgress}%
创建时间:{formatDate(exam.created_at)}
最后更新:{formatDate(exam.updated_at)}
请检查文档格式是否正确,或尝试重新上传。
浏览、搜索和查看该题库中的所有题目详情
上传新文档后,系统会自动解析题目并去除重复题目。