feat: judgment-anchored grading and per-question results

- Grader: inject judgment as pass criteria anchor in LLM prompt
- Grader: use followupHints for follow-up direction (not generic text)
- Grader: follow-up limit from followupHints.length instead of hardcoded 2
- Session: correctAnswer/judgment stored in questions, stripped during assessment
- Frontend: per-question results panel with choice / + judgment display
This commit is contained in:
Developer
2026-05-21 10:18:15 +08:00
parent 3993099907
commit 35b1c6c37d
3 changed files with 111 additions and 20 deletions
+61 -1
View File
@@ -13,7 +13,8 @@ import {
Star,
Award,
Trophy,
Trash2
Trash2,
XCircle
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useLanguage } from '../../contexts/LanguageContext';
@@ -823,6 +824,65 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
</div>
<div className="space-y-8">
{state?.questions && state.questions.length > 0 && (
<div>
<h4 className="flex items-center gap-2.5 text-lg font-black text-slate-900 mb-4">
<CheckCircle size={20} className="text-indigo-600" />
</h4>
<div className="space-y-4">
{state.questions.map((q: any, i: number) => {
const score = state.scores?.[q.id || (i + 1).toString()];
const isChoice = q.questionType === 'MULTIPLE_CHOICE';
const isCorrect = isChoice && q.correctAnswer && score >= 10;
return (
<div key={q.id || i} className="bg-white border border-slate-200 rounded-2xl p-5">
<div className="flex items-start gap-3">
<div className={cn(
"w-10 h-10 rounded-xl flex items-center justify-center shrink-0",
isChoice
? (isCorrect ? "bg-emerald-100 text-emerald-600" : "bg-red-100 text-red-600")
: score !== undefined ? "bg-indigo-100 text-indigo-600" : "bg-slate-100 text-slate-400"
)}>
{isChoice
? (isCorrect ? <CheckCircle size={20} /> : <XCircle size={20} />)
: <span className="text-sm font-black">{score !== undefined ? score : '?'}</span>
}
</div>
<div className="flex-1 min-w-0">
<p className="font-bold text-slate-800 text-sm leading-relaxed">{q.questionText}</p>
{isChoice && (
<div className="mt-2 flex flex-wrap gap-2 text-xs">
{q.options?.map((opt: string, oi: number) => {
const letter = String.fromCharCode(65 + oi);
const isAnswer = letter === q.correctAnswer;
return (
<span key={oi} className={cn(
"px-3 py-1 rounded-lg font-medium",
isAnswer ? "bg-emerald-100 text-emerald-700 border border-emerald-200" : "bg-slate-50 text-slate-500"
)}>
{letter}. {opt}
</span>
);
})}
</div>
)}
{q.judgment && (
<div className="mt-3 bg-blue-50/50 border border-blue-100 rounded-xl p-3">
<p className="text-xs text-slate-600 leading-relaxed">{q.judgment}</p>
</div>
)}
{!isChoice && score !== undefined && (
<span className="inline-block mt-2 text-xs text-slate-400">: {score}/10</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
)}
<div>
<h4 className="flex items-center gap-2.5 text-lg font-black text-slate-900 mb-4">
<FileText size={20} className="text-indigo-600" />