forked from hangshuo652/aurak
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:
@@ -51,6 +51,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
const [templates, setTemplates] = useState<AssessmentTemplate[]>([]);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
|
||||
const [timeCheck, setTimeCheck] = useState<{ totalTimeRemaining: number; questionTimeRemaining: number; isTotalTimeout: boolean; isQuestionTimeout: boolean } | null>(null);
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const isTimedOut = timeCheck?.isTotalTimeout || timeCheck?.isQuestionTimeout;
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -232,10 +233,18 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
};
|
||||
|
||||
const handleSubmitAnswer = async () => {
|
||||
if (!session || !inputValue.trim() || isLoading || isTimedOut) return;
|
||||
const currentQuestion = state?.questions?.[state.currentQuestionIndex || 0] as any;
|
||||
const isChoice = currentQuestion?.questionType === 'MULTIPLE_CHOICE' && currentQuestion?.options?.length > 0;
|
||||
|
||||
const answer = inputValue.trim();
|
||||
if (isChoice) {
|
||||
if (!selectedChoice || isLoading || isTimedOut) return;
|
||||
} else {
|
||||
if (!inputValue.trim() || isLoading || isTimedOut) return;
|
||||
}
|
||||
|
||||
const answer = isChoice ? selectedChoice! : inputValue.trim();
|
||||
setInputValue('');
|
||||
setSelectedChoice(null);
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setProcessStep(isZh ? '正在准备发送...' : isJa ? '送信準備中...' : 'Preparing to send...');
|
||||
@@ -507,6 +516,10 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
!(m.role === 'assistant' && (m.content?.toString().startsWith('Score:') || m.content?.toString().startsWith('得分:')))
|
||||
);
|
||||
|
||||
const currentQuestion = (state?.questions?.[state.currentQuestionIndex || 0] || {}) as any;
|
||||
const isCurrentChoice = currentQuestion.questionType === 'MULTIPLE_CHOICE' && currentQuestion.options?.length > 0;
|
||||
const optionLabels = ['A', 'B', 'C', 'D'];
|
||||
|
||||
const feedbackHistory = state?.feedbackHistory || [];
|
||||
const lastFeedbackMessage = feedbackHistory[feedbackHistory.length - 1];
|
||||
|
||||
@@ -586,6 +599,52 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
{t('timeLimitExceeded')}
|
||||
</div>
|
||||
)}
|
||||
{isCurrentChoice ? (
|
||||
<div className="max-w-3xl mx-auto space-y-3">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 font-bold uppercase tracking-wider mb-1">
|
||||
<span className="w-1 h-1 bg-indigo-400 rounded-full" />
|
||||
请选择一个选项
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
{currentQuestion.options.map((opt: string, i: number) => {
|
||||
const letter = optionLabels[i];
|
||||
const isSelected = selectedChoice === letter;
|
||||
return (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => !isTimedOut && setSelectedChoice(letter)}
|
||||
disabled={isTimedOut}
|
||||
className={cn(
|
||||
"w-full text-left px-5 py-4 rounded-2xl border-2 transition-all text-sm font-medium",
|
||||
isSelected
|
||||
? "border-indigo-500 bg-indigo-50 text-indigo-700 shadow-md"
|
||||
: "border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50",
|
||||
isTimedOut && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center justify-center w-7 h-7 rounded-xl text-xs font-black mr-3 shrink-0 border-2 border-current">
|
||||
{letter}
|
||||
</span>
|
||||
{opt}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmitAnswer}
|
||||
disabled={!selectedChoice || isLoading || isTimedOut}
|
||||
className={cn(
|
||||
"w-full mt-3 h-14 flex items-center justify-center gap-2 rounded-2xl transition-all shadow-lg text-white font-bold",
|
||||
!selectedChoice || isLoading || isTimedOut
|
||||
? "bg-slate-300 cursor-not-allowed"
|
||||
: "bg-indigo-600 hover:bg-indigo-700 active:scale-[0.97]"
|
||||
)}
|
||||
>
|
||||
{isLoading ? <Loader2 size={20} className="animate-spin" /> : <Send size={20} />}
|
||||
<span className="text-sm">确认答案</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-w-3xl mx-auto flex items-end gap-3">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
@@ -614,6 +673,7 @@ export const AssessmentView: React.FC<AssessmentViewProps> = ({
|
||||
<Send size={22} className={isLoading ? "animate-pulse" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user