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 (
);
}
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 (
);
}
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 ? t('editQuestion') : t('addQuestionTitle')}
)}
, document.body
)}
{createPortal(
{showGenerate && (
setShowGenerate(false)} className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" />
setGenerateForm({...generateForm, count: parseInt(e.target.value) || 5})} className="w-full px-5 py-4 bg-slate-50 border border-slate-200 rounded-[1.25rem] text-sm font-bold focus:ring-4 focus:ring-purple-500/10 focus:border-purple-500/50 outline-none transition-all" min={1} max={20} />
知识库内容已自动加载
)}
, document.body
)}
);
}