feat: end-to-end choice question support in assessment pipeline
- Data pathway: flow options through questions, answerKey in graph state - Interviewer: format MULTIPLE_CHOICE with A/B/C/D options - Grader: instant choice scoring (zero LLM), compare correctAnswer - AssessmentView: render choice buttons vs textarea based on questionType - Security: sanitizeStateForClient strips correctAnswer/judgment/answerKey - Bank detection: check PUBLISHED items (not PUBLISHED bank status) - Batch UI: select all / batch approve / batch reject on detail view
This commit is contained in:
@@ -76,6 +76,48 @@ export default function QuestionBankDetailView() {
|
||||
|
||||
const [generateForm, setGenerateForm] = useState({ count: 5, knowledgeBaseContent: '' });
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(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]);
|
||||
|
||||
@@ -304,10 +346,28 @@ export default function QuestionBankDetailView() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-black text-slate-900">{t('questionList')}</h2>
|
||||
<button onClick={() => { setShowAddItem(true); setEditingItem(null); setKeyPointsInput(''); setItemForm({ questionText: '', questionType: 'SHORT_ANSWER', keyPoints: [], difficulty: 'STANDARD', dimension: (dimensionOptions[0]?.value as any) || 'WORK_CAPABILITY' }); }}
|
||||
className="px-5 py-3 bg-blue-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95">
|
||||
<Plus size={16} /> {t('addQuestion')}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItemIds.size > 0 && (
|
||||
<>
|
||||
<button onClick={handleBatchApprove}
|
||||
className="px-4 py-2.5 bg-emerald-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-emerald-100 hover:bg-emerald-700 transition-all active:scale-95">
|
||||
<Check size={14} /> 通过所选 ({selectedItemIds.size})
|
||||
</button>
|
||||
<button onClick={handleBatchReject}
|
||||
className="px-4 py-2.5 bg-red-500 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-red-100 hover:bg-red-600 transition-all active:scale-95">
|
||||
<X size={14} /> 驳回所选
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button onClick={toggleSelectAll}
|
||||
className="px-4 py-2.5 bg-slate-100 text-slate-600 rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 hover:bg-slate-200 transition-all">
|
||||
{allSelected ? '取消全选' : '全选'}
|
||||
</button>
|
||||
<button onClick={() => { setShowAddItem(true); setEditingItem(null); setKeyPointsInput(''); setItemForm({ questionText: '', questionType: 'SHORT_ANSWER', keyPoints: [], difficulty: 'STANDARD', dimension: (dimensionOptions[0]?.value as any) || 'WORK_CAPABILITY' }); }}
|
||||
className="px-5 py-3 bg-blue-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95">
|
||||
<Plus size={16} /> {t('addQuestion')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
@@ -327,6 +387,11 @@ export default function QuestionBankDetailView() {
|
||||
className="bg-white border border-slate-200 rounded-2xl p-5 shadow-sm hover:shadow-md transition-all group relative overflow-hidden">
|
||||
<div className={`absolute top-0 right-0 w-40 h-40 rounded-full blur-3xl -mr-20 -mt-20 ${itemStat.blur}`} />
|
||||
<div className="relative z-10 flex items-start justify-between">
|
||||
{item.status === 'PENDING_REVIEW' && (
|
||||
<input type="checkbox" checked={selectedItemIds.has(item.id)}
|
||||
onChange={() => 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" />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2.5 flex-wrap">
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-slate-50 text-slate-600 text-[10px] font-bold rounded-lg border border-slate-100">{typeIcons[item.questionType]}{t(QUESTION_TYPES.find(qt => qt.value === item.questionType)?.labelKey || 'shortAnswer')}</span>
|
||||
|
||||
Reference in New Issue
Block a user