import React, { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { Brain, Send, Loader2, CheckCircle, AlertCircle, ChevronRight, History, ClipboardCheck, RefreshCcw, FileText, Star, Award, Trophy, Trash2, XCircle } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useLanguage } from '../../contexts/LanguageContext'; import { useConfirm } from '../../contexts/ConfirmContext'; import { assessmentService, AssessmentSession, AssessmentState } from '../../services/assessmentService'; import { knowledgeGroupService } from '../../services/knowledgeGroupService'; import { templateService } from '../../services/templateService'; import { KnowledgeGroup, AssessmentTemplate } from '../../types'; import { cn } from '../../src/utils/cn'; interface AssessmentViewProps { onLogout: () => void; onNavigate: (path: string) => void; isAdmin: boolean; } export const AssessmentView: React.FC = ({ onLogout, onNavigate, isAdmin }) => { const { language, t } = useLanguage(); const { confirm } = useConfirm(); const [groups, setGroups] = useState([]); const [selectedGroup, setSelectedGroup] = useState(null); const [session, setSession] = useState(null); const [state, setState] = useState(null); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [processStep, setProcessStep] = useState(''); const [error, setError] = useState(null); const [history, setHistory] = useState([]); const [loadingHistoryId, setLoadingHistoryId] = useState(null); const [showBasis, setShowBasis] = useState(false); const [templates, setTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null); const [selectedChoice, setSelectedChoice] = useState(null); const [autoSubmitted, setAutoSubmitted] = useState(false); const [showCertModal, setShowCertModal] = useState(false); const [certData, setCertData] = useState(null); // P0: Flagged questions for review const [flaggedQuestions, setFlaggedQuestions] = useState>(new Set()); // P0: Submit confirmation modal const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout; const messagesEndRef = useRef(null); useEffect(() => { const fetchGroups = async () => { try { const data = await knowledgeGroupService.getGroups(); setGroups(data); } catch (err) { console.error('Failed to fetch groups:', err); } }; const fetchTemplates = async () => { try { const data = await templateService.getAll(); setTemplates(data); } catch (err) { console.error('Failed to fetch templates:', err); } }; fetchGroups(); fetchTemplates(); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [state?.messages, isLoading]); const fetchHistory = async () => { try { const data = await assessmentService.getHistory(); setHistory(data); } catch (err) { console.error('Failed to fetch history:', err); } }; useEffect(() => { fetchHistory(); }, []); useEffect(() => { if (!session || session.status !== 'IN_PROGRESS') { setTimeCheck(null); return; } const checkTime = async () => { try { const data = await assessmentService.checkTimeLimits(session.id); setTimeCheck(data); if (data.isTotalTimeout || data.isQuestionTimeout) { setError(t('timeLimitExceeded')); if (!autoSubmitted && !isLoading) { setAutoSubmitted(true); await handleSubmitAnswer(true); } } } catch (err) { console.error('Failed to check time:', err); } }; checkTime(); const interval = setInterval(checkTime, 10000); return () => clearInterval(interval); }, [session]); const isZh = language === 'zh'; const isJa = language === 'ja'; const getStatusText = (node: string) => { const mapping: Record = { generator: 'statusGeneratingQuestions', grader: 'statusEvaluatingAnswer', interviewer: 'statusPreparingQuestion', analyzer: 'statusGeneratingReport', }; return t(mapping[node]) || t('statusProcessing'); }; const handleSelectHistory = async (histSession: AssessmentSession) => { if (isLoading) return; setLoadingHistoryId(histSession.id); setIsLoading(true); setError(null); try { const histState = await assessmentService.getSessionState(histSession.id); setState(histState); setSession(histSession); } catch (err: any) { if (histSession.status === 'IN_PROGRESS') { setError(t('cannotResumeInProgress')); } else { setError(err.message || 'Failed to load historical assessment'); } } finally { setIsLoading(false); setLoadingHistoryId(null); } }; const handleDeleteHistory = async (e: React.MouseEvent, histId: string) => { e.stopPropagation(); const confirmed = await confirm(t('confirmDeleteAssessment')); if (!confirmed) return; try { await assessmentService.deleteSession(histId); setHistory(prev => prev.filter(h => h.id !== histId)); if (session?.id === histId) { setSession(null); setState(null); } } catch (err: any) { console.error('Failed to delete history:', err); setError(t('deleteAssessmentFailed')); } }; const handleStartAssessment = async () => { if (!selectedTemplate) return; setIsLoading(true); setError(null); setProcessStep(isZh ? '正在初始化...' : isJa ? '初期化中...' : 'Initializing...'); try { const newSession = await assessmentService.startSession(selectedGroup || undefined, language, selectedTemplate || undefined); setSession(newSession); for await (const event of assessmentService.startSessionStream(newSession.id)) { if (event.type === 'node') { setProcessStep(getStatusText(event.node)); if (event.data) { setState(prev => { if (!prev) return event.data; const prevMessages = prev.messages || []; return { ...prev, ...event.data, messages: event.data.messages ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))] : prevMessages, feedbackHistory: event.data.feedbackHistory ? [...(prev.feedbackHistory || []), ...event.data.feedbackHistory.filter((fh: any) => !(prev.feedbackHistory || []).some((pfh: any) => pfh.content === fh.content))] : (prev.feedbackHistory || []), scores: { ...(prev.scores || {}), ...(event.data.scores || {}) } } as any; }); } } else if (event.type === 'final') { setState(event.data); } } } catch (err: any) { setError(err.message || 'Failed to start assessment'); } finally { setIsLoading(false); setProcessStep(''); } }; const handleRetry = async () => { if (!session) return; setIsLoading(true); setError(null); setProcessStep(isZh ? '正在重新尝试生成...' : isJa ? '再生成中...' : 'Retrying generation...'); try { for await (const event of assessmentService.startSessionStream(session.id)) { if (event.type === 'node') { setProcessStep(getStatusText(event.node)); } else if (event.type === 'final') { setState(event.data); } } } catch (err: any) { setError(err.message || 'Retry failed'); } finally { setIsLoading(false); setProcessStep(''); } }; // P0: Toggle flag for current question const toggleFlag = () => { const idx = state?.currentQuestionIndex ?? 0; setFlaggedQuestions(prev => { const next = new Set(prev); if (next.has(idx)) next.delete(idx); else next.add(idx); return next; }); }; // P0: Confirm & submit const confirmAndSubmit = async () => { const totalQs = state?.questions?.length || 0; const answered = state?.scores ? Object.keys(state.scores).length : 0; if (answered < totalQs && totalQs > 0) { setShowSubmitConfirm(true); return; } await handleSubmitAnswer(); }; const handleSubmitAnswer = async (forced = false) => { const currentQuestion = state?.questions?.[state.currentQuestionIndex || 0] as any; const isChoice = currentQuestion?.questionType === 'MULTIPLE_CHOICE' && currentQuestion?.options?.length > 0; if (!forced) { if (isChoice) { if (!selectedChoice || isLoading || isTimedOut) return; } else { if (!inputValue.trim() || isLoading || isTimedOut) return; } } const answer = isChoice ? (selectedChoice || '') : inputValue.trim(); setInputValue(''); setSelectedChoice(null); setIsLoading(true); setError(null); setProcessStep(isZh ? '正在准备发送...' : isJa ? '送信準備中...' : 'Preparing to send...'); try { setState(prev => ({ ...prev!, messages: [ ...(prev?.messages || []), { role: 'user' as const, content: answer, timestamp: Date.now() } ] })); for await (const event of assessmentService.submitAnswerStream(session.id, answer, language)) { if (event.type === 'node') { setProcessStep(getStatusText(event.node)); if (event.data) { setState(prev => { if (!prev) return event.data; const prevMessages = prev.messages || []; const mergedMessages = event.data.messages ? [...prevMessages, ...event.data.messages.filter((m: any) => !prevMessages.some((pm: any) => (m.id && pm.id === m.id) || (pm.content === m.content && pm.role === m.role)))] : prevMessages; return { ...prev, ...event.data, messages: mergedMessages, feedbackHistory: event.data.feedbackHistory ? [...(prev.feedbackHistory || []), ...event.data.feedbackHistory.filter((fh: any) => !(prev.feedbackHistory || []).some((pfh: any) => pfh.content === fh.content))] : (prev.feedbackHistory || []), scores: { ...(prev.scores || {}), ...(event.data.scores || {}) } } as any; }); } } else if (event.type === 'final') { setState(event.data); if (event.data.status === 'COMPLETED') { setSession(prev => prev ? { ...prev, status: 'COMPLETED' } : null); fetchHistory(); } else if (event.data.currentQuestionIndex !== undefined) { assessmentService.nextQuestion(session.id).catch(() => {}); } } } } catch (err: any) { setError(err.message || 'Failed to submit answer'); } finally { setIsLoading(false); setProcessStep(''); } }; const renderHeader = () => (

{t('assessmentTitle')}

{t('assessmentDesc')}

{session && (
{session.status === 'IN_PROGRESS' ? t('inProgress') : t('statusReadyFragment')}
)}
); const renderSetup = () => (
{/* Main Setup Content */}

{t('readyForAssessment')}

{t('readyForAssessmentDesc')}

{templates.map(template => ( ))}
{error && (

{error}

)}
{t('aiPoweredAnalysis')}
{t('masteryScoring')}
{/* Assessment History Sidebar */} {history.length > 0 && (

{t('recentAssessments')}

{history.map(hist => (
{hist.knowledgeBase?.name || hist.knowledgeGroup?.name || t('assessmentTitle')}
{hist.finalScore !== null && hist.finalScore !== undefined ? `${Math.round(hist.finalScore * 10) / 10}/10` : t('inProgress')} {new Date(hist.createdAt).toLocaleDateString()}
))}
)}
); const renderAssessment = () => { const currentIndex = state?.currentQuestionIndex || 0; const totalQuestions = state?.questions?.length || 0; // 如果currentIndex已达到或超过题目数量,说明已完成 const displayNo = currentIndex >= totalQuestions ? totalQuestions : currentIndex + 1; console.log('[AssessmentView] Counter:', { displayNo, totalQuestions, currentIndex }); const progressLabel = totalQuestions > 0 ? t('questionProgress', displayNo, totalQuestions) : t('initializingQuestion', displayNo); const messages = state?.messages || []; const filteredMessages = messages.filter(m => m.role !== 'system' && !(m.role === 'assistant' && (m.content?.toString().startsWith('Score:') || m.content?.toString().startsWith('得分:'))) ); const currentQuestion = (state?.questions?.[state.currentQuestionIndex || 0] || {}) as any; const isCurrentChoice = currentQuestion.questionType === 'MULTIPLE_CHOICE' && currentQuestion.options?.length > 0; const optionLabels = ['A', 'B', 'C', 'D']; const feedbackHistory = state?.feedbackHistory || []; const lastFeedbackMessage = feedbackHistory[feedbackHistory.length - 1]; const feedbackMatch = lastFeedbackMessage?.content?.toString().match(/(?:Score|得分): (\d+)\/10\n\n(?:Feedback|反馈): ([\s\S]*)/i); const latestScore = feedbackMatch ? feedbackMatch[1] : null; const latestFeedback = feedbackMatch ? feedbackMatch[2] : (lastFeedbackMessage?.content || null); return (
{/* Left: Chat Area */}
{progressLabel} {/* P0: Question nav dots */} {state?.questions && state.questions.length > 1 && (
{state.questions.map((_: any, qi: number) => (
))}
)} {isLoading && (
{processStep || t('aiIsProcessing')} )} {timeCheck && (
{Math.floor(timeCheck.totalTimeRemaining / 60)}:{String(timeCheck.totalTimeRemaining % 60).padStart(2, '0')}
)}
{filteredMessages.map((msg, idx) => (
{msg.content}
{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
))} {isLoading && (
)}
{isTimedOut && (
{t('timeLimitExceeded')}
)} {isCurrentChoice ? (
请选择一个选项
{currentQuestion.options.map((opt: string, i: number) => { const letter = optionLabels[i]; const isSelected = selectedChoice === letter; return ( ); })}
) : (