F1-F10: audit fixes (dimension normalize, passingScore scale, DB defaults, onDelete, item status filter, timeout event type, userId privacy) + generator.node.ts strict prompt rules (anti-hallucination)
This commit is contained in:
@@ -34,7 +34,7 @@ export const WorkspaceLayout: React.FC<WorkspaceLayoutProps> = ({
|
||||
appMode={appMode}
|
||||
onSwitchMode={onSwitchMode}
|
||||
/>
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
<div className="flex-1 overflow-auto relative">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +108,11 @@ export default function QuestionBankDetailView() {
|
||||
}
|
||||
};
|
||||
|
||||
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' },
|
||||
@@ -122,18 +127,12 @@ export default function QuestionBankDetailView() {
|
||||
if (!itemForm.questionText.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await questionBankService.createItem(bankId, {
|
||||
...itemForm,
|
||||
keyPoints: keyPointsInput.split('\n').filter(k => k.trim()),
|
||||
});
|
||||
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);
|
||||
}
|
||||
} catch (err: any) { showError(err.message || t('actionFailed'));
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleUpdateItem = async (e: React.FormEvent) => {
|
||||
@@ -141,57 +140,38 @@ export default function QuestionBankDetailView() {
|
||||
if (!editingItem || !itemForm.questionText.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await questionBankService.updateItem(bankId, editingItem.id, {
|
||||
...itemForm,
|
||||
keyPoints: keyPointsInput.split('\n').filter(k => k.trim()),
|
||||
});
|
||||
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);
|
||||
}
|
||||
} 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'));
|
||||
}
|
||||
try { await questionBankService.deleteItem(bankId, itemId); showSuccess(t('questionDeleted')); fetchData();
|
||||
} catch (err: any) { showError(err.message || t('actionFailed')); }
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
try {
|
||||
const result = await questionBankService.generateQuestions(bankId, generateForm.count, generateForm.knowledgeBaseContent);
|
||||
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);
|
||||
}
|
||||
} 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'));
|
||||
}
|
||||
try { await questionBankService.submitForReview(bankId); showSuccess(t('bankSubmittedForReview')); fetchData();
|
||||
} catch (err: any) { showError(err.message || t('actionFailed')); }
|
||||
};
|
||||
|
||||
const handlePublish = async () => {
|
||||
@@ -201,48 +181,26 @@ export default function QuestionBankDetailView() {
|
||||
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);
|
||||
}
|
||||
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'));
|
||||
}
|
||||
} 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'));
|
||||
}
|
||||
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'));
|
||||
}
|
||||
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,
|
||||
});
|
||||
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);
|
||||
};
|
||||
@@ -264,8 +222,7 @@ export default function QuestionBankDetailView() {
|
||||
<ChevronLeft size={18} /><span className="text-xs font-black uppercase tracking-widest">{t('backToBankList')}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-3 text-red-500 bg-red-50 rounded-2xl p-6 border border-red-100">
|
||||
<AlertCircle size={20} />
|
||||
<span className="text-sm font-bold">{error}</span>
|
||||
<AlertCircle size={20} /><span className="text-sm font-bold">{error}</span>
|
||||
<button onClick={fetchData} className="ml-auto text-xs font-black text-red-600 hover:text-red-700 uppercase tracking-widest">{t('retry')}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -291,7 +248,7 @@ export default function QuestionBankDetailView() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 overflow-y-auto h-full">
|
||||
<button onClick={() => navigate('/question-banks')} className="flex items-center gap-2 text-slate-400 hover:text-slate-600 transition-colors">
|
||||
<ChevronLeft size={18} /><span className="text-xs font-black uppercase tracking-widest">{t('backToBankList')}</span>
|
||||
</button>
|
||||
@@ -326,7 +283,7 @@ export default function QuestionBankDetailView() {
|
||||
<Check size={16} /> {bank?.status === 'PENDING_REVIEW' ? t('approve') : t('republish')}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setShowGenerate(true)} className="px-5 py-3 bg-purple-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95">
|
||||
<button onClick={openGenerateModal} className="px-5 py-3 bg-purple-600 text-white rounded-xl text-xs font-black uppercase tracking-widest flex items-center gap-2 shadow-lg shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95">
|
||||
<Sparkles size={16} /> {t('aiGenerate')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -355,9 +312,7 @@ export default function QuestionBankDetailView() {
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="bg-slate-50 rounded-[2rem] border-2 border-dashed border-slate-200 p-16 text-center">
|
||||
<div className="w-14 h-14 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<FileText size={28} className="text-slate-300" />
|
||||
</div>
|
||||
<div className="w-14 h-14 bg-slate-100 rounded-2xl flex items-center justify-center mx-auto mb-4"><FileText size={28} className="text-slate-300" /></div>
|
||||
<p className="text-slate-400 font-black uppercase tracking-widest text-xs mb-1">{t('noQuestions')}</p>
|
||||
<p className="text-slate-300 text-xs">{t('noQuestionsDesc')}</p>
|
||||
</div>
|
||||
@@ -374,21 +329,10 @@ export default function QuestionBankDetailView() {
|
||||
<div className="relative z-10 flex items-start justify-between">
|
||||
<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>
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100">
|
||||
<Hash size={10} />
|
||||
{t(DIFFICULTIES.find(d => d.value === item.difficulty)?.labelKey || 'standard')}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-50 text-purple-600 text-[10px] font-bold rounded-lg border border-purple-100">
|
||||
<Brain size={10} />
|
||||
{dimensionOptions.find(d => d.value === item.dimension)?.label || item.dimension}
|
||||
</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2.5 py-1 text-[10px] font-black uppercase tracking-widest rounded-full border ${itemStat.bg} ${itemStat.text} ${itemStat.border}`}>
|
||||
{itemStat.icon}{itemStat.label}
|
||||
</span>
|
||||
<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>
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-blue-50 text-blue-600 text-[10px] font-bold rounded-lg border border-blue-100"><Hash size={10} />{t(DIFFICULTIES.find(d => d.value === item.difficulty)?.labelKey || 'standard')}</span>
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-purple-50 text-purple-600 text-[10px] font-bold rounded-lg border border-purple-100"><Brain size={10} />{dimensionOptions.find(d => d.value === item.dimension)?.label || item.dimension}</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2.5 py-1 text-[10px] font-black uppercase tracking-widest rounded-full border ${itemStat.bg} ${itemStat.text} ${itemStat.border}`}>{itemStat.icon}{itemStat.label}</span>
|
||||
</div>
|
||||
<p className="font-bold text-slate-900 leading-relaxed">{item.questionText}</p>
|
||||
{item.keyPoints.length > 0 && (
|
||||
@@ -398,18 +342,14 @@ export default function QuestionBankDetailView() {
|
||||
</div>
|
||||
)}
|
||||
{item.basis && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[10px] text-slate-400">
|
||||
<FileText size={10} /><span className="font-medium">{t('basis')}</span><span>{item.basis}</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[10px] text-slate-400"><FileText size={10} /><span className="font-medium">{t('basis')}</span><span>{item.basis}</span></div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 ml-4 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{item.status === 'PENDING_REVIEW' && (
|
||||
<>
|
||||
<button onClick={() => handleApproveItem(item.id)} className="p-2 text-emerald-600 hover:bg-emerald-50 rounded-xl transition-all" title={t('approve')}><Check size={15} /></button>
|
||||
<button onClick={() => handleRejectItem(item.id)} className="p-2 text-red-500 hover:bg-red-50 rounded-xl transition-all" title={t('rejected')}><X size={15} /></button>
|
||||
</>
|
||||
)}
|
||||
{item.status === 'PENDING_REVIEW' && (<>
|
||||
<button onClick={() => handleApproveItem(item.id)} className="p-2 text-emerald-600 hover:bg-emerald-50 rounded-xl transition-all" title={t('approve')}><Check size={15} /></button>
|
||||
<button onClick={() => handleRejectItem(item.id)} className="p-2 text-red-500 hover:bg-red-50 rounded-xl transition-all" title={t('rejected')}><X size={15} /></button>
|
||||
</>)}
|
||||
<button onClick={() => openEditItem(item)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-xl transition-all" title={t('edit')}><Edit2 size={15} /></button>
|
||||
<button onClick={() => handleDeleteItem(item.id)} className="p-2 text-red-500 hover:bg-red-50 rounded-xl transition-all" title={t('delete')}><Trash2 size={15} /></button>
|
||||
</div>
|
||||
@@ -429,60 +369,37 @@ export default function QuestionBankDetailView() {
|
||||
<motion.div initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
className="w-full max-w-xl bg-white rounded-[2.5rem] shadow-2xl relative z-10 overflow-hidden">
|
||||
<div className="p-8 pb-4 flex items-center justify-between border-b border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center">{editingItem ? <Edit2 size={24} /> : <Plus size={24} />}</div>
|
||||
<h3 className="text-xl font-black text-slate-900">{editingItem ? t('editQuestion') : t('addQuestionTitle')}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-3"><div className="w-12 h-12 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center">{editingItem ? <Edit2 size={24} /> : <Plus size={24} />}</div>
|
||||
<h3 className="text-xl font-black text-slate-900">{editingItem ? t('editQuestion') : t('addQuestionTitle')}</h3></div>
|
||||
<button onClick={closeItemForm} className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-xl transition-all"><X size={20} /></button>
|
||||
</div>
|
||||
<form id="item-form" onSubmit={editingItem ? handleUpdateItem : handleCreateItem} className="p-8 space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><FileText size={12} className="text-blue-500" /> {t('questionContent')} <span className="text-red-500">*</span></label>
|
||||
<textarea value={itemForm.questionText} onChange={(e) => setItemForm({ ...itemForm, questionText: e.target.value })}
|
||||
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-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300"
|
||||
placeholder={t('questionContent')} rows={3} required />
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><FileText size={12} className="text-blue-500" /> {t('questionContent')} <span className="text-red-500">*</span></label>
|
||||
<textarea value={itemForm.questionText} onChange={(e) => setItemForm({...itemForm, questionText: e.target.value})} 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-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" placeholder={t('questionContent')} rows={3} required />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Layers size={12} className="text-blue-500" /> {t('questionType')}</label>
|
||||
<select value={itemForm.questionType} onChange={(e) => setItemForm({ ...itemForm, questionType: e.target.value as any })}
|
||||
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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">
|
||||
{QUESTION_TYPES.map(qt => <option key={qt.value} value={qt.value}>{t(qt.labelKey)}</option>)}
|
||||
</select>
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Layers size={12} className="text-blue-500" /> {t('questionType')}</label>
|
||||
<select value={itemForm.questionType} onChange={(e) => setItemForm({...itemForm, questionType: e.target.value as any})} 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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">{ QUESTION_TYPES.map(qt => <option key={qt.value} value={qt.value}>{t(qt.labelKey)}</option>) }</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Hash size={12} className="text-blue-500" /> {t('difficultyDistribution')}</label>
|
||||
<select value={itemForm.difficulty} onChange={(e) => setItemForm({ ...itemForm, difficulty: e.target.value as any })}
|
||||
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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">
|
||||
{DIFFICULTIES.map(d => <option key={d.value} value={d.value}>{t(d.labelKey)}</option>)}
|
||||
</select>
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Hash size={12} className="text-blue-500" /> {t('difficultyDistribution')}</label>
|
||||
<select value={itemForm.difficulty} onChange={(e) => setItemForm({...itemForm, difficulty: e.target.value as any})} 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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">{ DIFFICULTIES.map(d => <option key={d.value} value={d.value}>{t(d.labelKey)}</option>) }</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Brain size={12} className="text-blue-500" /> {t('dimension')}</label>
|
||||
<select value={itemForm.dimension} onChange={(e) => setItemForm({ ...itemForm, dimension: e.target.value as any })}
|
||||
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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">
|
||||
{dimensionOptions.map(d => <option key={d.value} value={d.value}>{d.label}</option>)}
|
||||
</select>
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Brain size={12} className="text-blue-500" /> {t('dimension')}</label>
|
||||
<select value={itemForm.dimension} onChange={(e) => setItemForm({...itemForm, dimension: e.target.value as any})} 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-blue-500/10 focus:border-blue-500/50 outline-none transition-all cursor-pointer">{ dimensionOptions.map(d => <option key={d.value} value={d.value}>{d.label}</option>) }</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><AlertCircle size={12} className="text-blue-500" /> {t('gradingPoints')}</label>
|
||||
<textarea value={keyPointsInput} onChange={(e) => setKeyPointsInput(e.target.value)}
|
||||
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-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300"
|
||||
placeholder={'1\n2\n3'} rows={4} />
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><AlertCircle size={12} className="text-blue-500" /> {t('gradingPoints')}</label>
|
||||
<textarea value={keyPointsInput} onChange={(e) => setKeyPointsInput(e.target.value)} 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-blue-500/10 focus:border-blue-500/50 outline-none transition-all placeholder:text-slate-300" placeholder={'1\n2\n3'} rows={4} />
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<button type="button" onClick={closeItemForm} className="px-6 py-4 text-sm font-black text-slate-500 hover:text-slate-700 transition-colors">{t('cancel')}</button>
|
||||
<button type="submit" form="item-form" disabled={saving}
|
||||
className="px-10 py-4 bg-blue-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95 flex items-center gap-2">
|
||||
{saving && <Loader2 size={16} className="animate-spin" />}{saving ? t('saving') : (editingItem ? t('save') : t('addQuestion'))}</button>
|
||||
<button type="submit" form="item-form" disabled={saving} className="px-10 py-4 bg-blue-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-blue-100 hover:bg-blue-700 transition-all active:scale-95 flex items-center gap-2">{saving && <Loader2 size={16} className="animate-spin" />}{saving ? t('saving') : (editingItem ? t('save') : t('addQuestion'))}</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
</AnimatePresence>, document.body
|
||||
)}
|
||||
|
||||
{createPortal(
|
||||
@@ -493,36 +410,25 @@ export default function QuestionBankDetailView() {
|
||||
<motion.div initial={{ opacity: 0, scale: 0.9, y: 20 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
className="w-full max-w-md bg-white rounded-[2.5rem] shadow-2xl relative z-10 overflow-hidden">
|
||||
<div className="p-8 pb-4 flex items-center justify-between border-b border-slate-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center"><Sparkles size={24} /></div>
|
||||
<h3 className="text-xl font-black text-slate-900">{t('aiGenerateTitle')}</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-3"><div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center"><Sparkles size={24} /></div>
|
||||
<h3 className="text-xl font-black text-slate-900">{t('aiGenerateTitle')}</h3></div>
|
||||
<button onClick={() => setShowGenerate(false)} className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-50 rounded-xl transition-all"><X size={20} /></button>
|
||||
</div>
|
||||
<div className="p-8 space-y-5">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Hash size={12} className="text-purple-500" /> {t('generateCount')}</label>
|
||||
<input type="number" value={generateForm.count} onChange={(e) => 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} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><FileText size={12} className="text-purple-500" /> {t('knowledgeBaseContentOptional')}</label>
|
||||
<textarea value={generateForm.knowledgeBaseContent} onChange={(e) => setGenerateForm({ ...generateForm, knowledgeBaseContent: e.target.value })}
|
||||
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 placeholder:text-slate-300"
|
||||
placeholder={t('knowledgeBaseContentOptional')} rows={4} />
|
||||
<div className="space-y-1.5"><label className="text-[10px] font-black text-slate-400 uppercase tracking-widest px-1 ml-1 flex items-center gap-2"><Hash size={12} className="text-purple-500" /> {t('generateCount')}</label>
|
||||
<input type="number" value={generateForm.count} onChange={(e) => 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} />
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 px-1">知识库内容已自动加载</p>
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button onClick={() => setShowGenerate(false)} className="flex-1 px-6 py-4 text-sm font-black text-slate-500 hover:text-slate-700 transition-colors">{t('cancel')}</button>
|
||||
<button onClick={handleGenerate} disabled={generating}
|
||||
className="flex-1 px-6 py-4 bg-purple-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95 flex items-center justify-center gap-2">
|
||||
<button onClick={handleGenerate} disabled={generating} className="flex-1 px-6 py-4 bg-purple-600 text-white rounded-[1.25rem] font-black uppercase tracking-widest text-xs shadow-xl shadow-purple-100 hover:bg-purple-700 transition-all active:scale-95 flex items-center justify-center gap-2">
|
||||
{generating ? <><Loader2 size={16} className="animate-spin" /> {t('generating')}</> : <><Sparkles size={16} /> {t('generate')}</>}</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
</AnimatePresence>, document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function QuestionBankView({ isAdmin: _isAdmin }: QuestionBankView
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 overflow-y-auto h-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-slate-900">{t('questionBankManagement')}</h1>
|
||||
|
||||
Reference in New Issue
Block a user