import React, { useState, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { useParams, useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { ChevronLeft, Plus, Sparkles, Send, Check, X, XCircle, Clock, Trash2, Edit2, FileText, Loader2, BookOpen, Brain, AlertCircle, Hash, Layers } from 'lucide-react'; import { questionBankService, QuestionBank, QuestionBankItem, CreateQuestionBankItemDto } from '../../services/questionBankService'; import { templateService } from '../../services/templateService'; import { AssessmentTemplate } from '../../types'; import { useToast } from '../../contexts/ToastContext'; import { useConfirm } from '../../contexts/ConfirmContext'; import { useLanguage } from '../../contexts/LanguageContext'; const QUESTION_TYPES = [ { value: 'SHORT_ANSWER', labelKey: 'shortAnswer' as const }, { value: 'MULTIPLE_CHOICE', labelKey: 'multipleChoice' as const }, { value: 'TRUE_FALSE', labelKey: 'trueFalse' as const }, ]; const DIFFICULTIES = [ { value: 'STANDARD', labelKey: 'standard' as const }, { value: 'ADVANCED', labelKey: 'advanced' as const }, { value: 'SPECIALIST', labelKey: 'specialist' as const }, ]; type TypeIcon = { [key: string]: React.ReactNode }; const typeIcons: TypeIcon = { SHORT_ANSWER: , MULTIPLE_CHOICE: , TRUE_FALSE: , }; export default function QuestionBankDetailView() { const { id: bankId } = useParams<{ id: string }>(); const navigate = useNavigate(); const { t } = useLanguage(); const { showSuccess, showError } = useToast(); const { confirm } = useConfirm(); if (!bankId) { return (
{t('invalidBankId')}
); } const [bank, setBank] = useState(null); const [items, setItems] = useState([]); const [templates, setTemplates] = useState([]); const [template, setTemplate] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [showAddItem, setShowAddItem] = useState(false); const [showGenerate, setShowGenerate] = useState(false); const [editingItem, setEditingItem] = useState(null); const [itemForm, setItemForm] = useState({ questionText: '', questionType: 'SHORT_ANSWER', keyPoints: [], difficulty: 'STANDARD', dimension: 'WORK_CAPABILITY', }); const [keyPointsInput, setKeyPointsInput] = useState(''); const [generateForm, setGenerateForm] = useState({ count: 5, knowledgeBaseContent: '' }); const [generating, setGenerating] = useState(false); const [selectedItemIds, setSelectedItemIds] = useState>(new Set()); const selectableItems = items.filter(i => i.status === 'PENDING_REVIEW'); const allSelected = selectableItems.length > 0 && selectableItems.every(i => selectedItemIds.has(i.id)); const toggleSelectAll = () => { if (allSelected) { setSelectedItemIds(new Set()); } else { setSelectedItemIds(new Set(selectableItems.map(i => i.id))); } }; const toggleSelectItem = (itemId: string) => { setSelectedItemIds(prev => { const next = new Set(prev); if (next.has(itemId)) next.delete(itemId); else next.add(itemId); return next; }); }; const handleBatchApprove = async () => { const ids = Array.from(selectedItemIds); if (ids.length === 0) return; try { await questionBankService.batchReviewItems(bankId, ids, true); showSuccess(`已通过 ${ids.length} 道题目`); setSelectedItemIds(new Set()); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const handleBatchReject = async () => { const ids = Array.from(selectedItemIds); if (ids.length === 0) return; try { await questionBankService.batchReviewItems(bankId, ids, false); showSuccess(`已驳回 ${ids.length} 道题目`); setSelectedItemIds(new Set()); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; useEffect(() => { fetchData(); fetchTemplates(); }, [bankId]); const fetchData = async () => { try { setLoading(true); const bankData = await questionBankService.getBank(bankId); setBank(bankData); const itemsData = await questionBankService.getBankItems(bankId); setItems(itemsData); } catch (err: any) { setError(err.message || t('actionFailed')); showError(err.message || t('actionFailed')); } finally { setLoading(false); } }; const fetchTemplates = async () => { try { const data = await templateService.getAll(); setTemplates(data); const bankData = await questionBankService.getBank(bankId); if (bankData.templateId) { const tpl = data.find(tpl => tpl.id === bankData.templateId); setTemplate(tpl || null); } } catch { // silent } }; const openGenerateModal = () => { setShowGenerate(true); setGenerateForm({ count: 5, knowledgeBaseContent: '' }); }; const dimensionOptions = template?.dimensions?.map(d => ({ value: d.name || d.label, label: d.label || d.name })) || [ { value: 'PROMPT', label: 'Prompt' }, { value: 'LLM', label: 'LLM' }, { value: 'IDE', label: 'IDE' }, { value: 'DEV_PATTERN', label: 'Dev Pattern' }, { value: 'WORK_CAPABILITY', label: 'Work Capability' }, ]; const handleCreateItem = async (e: React.FormEvent) => { e.preventDefault(); if (!itemForm.questionText.trim()) return; setSaving(true); try { await questionBankService.createItem(bankId, { ...itemForm, keyPoints: keyPointsInput.split('\n').filter(k => k.trim()) }); closeItemForm(); showSuccess(t('questionAdded')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } finally { setSaving(false); } }; const handleUpdateItem = async (e: React.FormEvent) => { e.preventDefault(); if (!editingItem || !itemForm.questionText.trim()) return; setSaving(true); try { await questionBankService.updateItem(bankId, editingItem.id, { ...itemForm, keyPoints: keyPointsInput.split('\n').filter(k => k.trim()) }); closeItemForm(); showSuccess(t('questionUpdated')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } finally { setSaving(false); } }; const handleDeleteItem = async (itemId: string) => { const ok = await confirm({ message: t('confirmDeleteQuestion'), confirmLabel: t('delete'), cancelLabel: t('cancel') }); if (!ok) return; try { await questionBankService.deleteItem(bankId, itemId); showSuccess(t('questionDeleted')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const handleGenerate = async () => { setGenerating(true); try { await questionBankService.generateQuestions(bankId, generateForm.count, generateForm.knowledgeBaseContent); setShowGenerate(false); setGenerateForm({ count: 5, knowledgeBaseContent: '' }); showSuccess(t('generatedQuestions').replace('$1', String(generateForm.count))); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } finally { setGenerating(false); } }; const handleSubmitForReview = async () => { const ok = await confirm({ message: t('confirmSubmitReview'), confirmLabel: t('submitForReview'), cancelLabel: t('cancel') }); if (!ok) return; try { await questionBankService.submitForReview(bankId); showSuccess(t('bankSubmittedForReview')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const handlePublish = async () => { const isPendingReview = bank?.status === 'PENDING_REVIEW'; const label = isPendingReview ? t('approve') : t('republish'); const msg = isPendingReview ? t('confirmApproveBank') : t('confirmRepublishBank'); const ok = await confirm({ message: msg, confirmLabel: label, cancelLabel: t('cancel') }); if (!ok) return; try { if (isPendingReview) await questionBankService.approveBank(bankId); else await questionBankService.publishBank(bankId); showSuccess(isPendingReview ? t('bankApproved') : t('bankRepublished')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const handleApproveItem = async (itemId: string) => { try { await questionBankService.updateItem(bankId, itemId, { status: 'PUBLISHED' } as any); showSuccess(t('questionApproved')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const handleRejectItem = async (itemId: string) => { try { await questionBankService.batchReviewItems(bankId, [itemId], false); showSuccess(t('questionReturned')); fetchData(); } catch (err: any) { showError(err.message || t('actionFailed')); } }; const openEditItem = (item: QuestionBankItem) => { setEditingItem(item); setItemForm({ questionText: item.questionText, questionType: item.questionType, options: item.options || [], keyPoints: item.keyPoints, difficulty: item.difficulty, dimension: item.dimension }); setKeyPointsInput(item.keyPoints.join('\n')); setShowAddItem(true); }; const closeItemForm = () => { setShowAddItem(false); setEditingItem(null); }; if (loading) { return (
); } if (error) { return (
{error}
); } const pendingItems = items.filter(i => i.status === 'PENDING_REVIEW'); const publishedItems = items.filter(i => i.status === 'PUBLISHED'); const statusColors: Record = { PUBLISHED: { bg: 'bg-emerald-50', text: 'text-emerald-600', border: 'border-emerald-200/50', label: t('published'), blur: 'bg-emerald-500/5', icon: }, PENDING_REVIEW: { bg: 'bg-amber-50', text: 'text-amber-600', border: 'border-amber-200/50', label: t('pendingReview'), blur: 'bg-amber-500/5', icon: }, DRAFT: { bg: 'bg-slate-50', text: 'text-slate-500', border: 'border-slate-200/50', label: t('draft'), blur: 'bg-blue-500/5', icon: }, REJECTED: { bg: 'bg-red-50', text: 'text-red-500', border: 'border-red-200/50', label: t('rejected'), blur: 'bg-red-500/5', icon: }, }; const bankStatus = statusColors[bank?.status || 'DRAFT'] || statusColors.DRAFT; const statCards = [ { label: t('questionList'), value: items.length, icon: , classes: 'bg-slate-50 border-slate-200/50 text-slate-700' }, { label: t('published'), value: publishedItems.length, icon: , classes: 'bg-emerald-50 border-emerald-200/50 text-emerald-700' }, { label: t('pendingReview'), value: pendingItems.length, icon: , classes: 'bg-amber-50 border-amber-200/50 text-amber-700' }, ]; return (

{bank?.name}

{bank?.description || t('noDescription')}

{template && ( {template.name} )} {bankStatus.icon}{bankStatus.label}
{bank?.status === 'DRAFT' && ( )} {(bank?.status === 'PENDING_REVIEW' || bank?.status === 'REJECTED') && ( )}
{statCards.map((stat, i) => (
{stat.label} {stat.icon}
{stat.value}
))}

{t('questionList')}

{selectedItemIds.size > 0 && ( <> )}
{items.length === 0 ? (

{t('noQuestions')}

{t('noQuestionsDesc')}

) : (
{items.map((item, idx) => { const itemStat = item.status === 'PUBLISHED' ? statusColors.PUBLISHED : statusColors.PENDING_REVIEW; return (
{item.status === 'PENDING_REVIEW' && ( toggleSelectItem(item.id)} className="mt-1.5 mr-3 w-4 h-4 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500 shrink-0 cursor-pointer" /> )}
{typeIcons[item.questionType]}{t(QUESTION_TYPES.find(qt => qt.value === item.questionType)?.labelKey || 'shortAnswer')} {t(DIFFICULTIES.find(d => d.value === item.difficulty)?.labelKey || 'standard')} {dimensionOptions.find(d => d.value === item.dimension)?.label || item.dimension} {itemStat.icon}{itemStat.label}

{item.questionText}

{item.questionType === 'MULTIPLE_CHOICE' && item.options && item.options.length > 0 && (
{item.options.map((opt, i) => { const letter = String.fromCharCode(65 + i); const isCorrect = item.correctAnswer === letter; const displayText = opt.slice(1); return (
{letter} {displayText} {isCorrect && }
); })}
)} {item.judgment && (
{item.questionType === 'MULTIPLE_CHOICE' ? '解析' : '判定依据'}

{item.judgment}

)} {item.questionType === 'SHORT_ANSWER' && item.followupHints && item.followupHints.length > 0 && (
追问方向 {item.followupHints.map((hint, i) => #{i + 1} {hint})}
)} {item.keyPoints.length > 0 && (
{t('gradingPoints')} {item.keyPoints.map((kp, i) => {kp})}
)} {item.basis && (
{t('basis')}{item.basis}
)}
{item.status === 'PENDING_REVIEW' && (<> )}
); })}
)} {createPortal( {(showAddItem || editingItem) && (
{editingItem ? : }

{editingItem ? t('editQuestion') : t('addQuestionTitle')}