rag-kb-system / src /components /InteractiveQuiz.tsx
duqing2026's picture
Initial commit for Hugging Face
6312023
"use client";
import { useState } from 'react';
import { CheckCircle, XCircle, AlertCircle, Share2, Copy, Check, ChevronDown, ChevronUp } from 'lucide-react';
export interface QuizQuestion {
id: number;
question: string;
options: string[];
correctAnswer: number; // 0-based index
explanation: string;
}
interface InteractiveQuizProps {
questions: QuizQuestion[];
allowSharing?: boolean;
}
export function InteractiveQuiz({ questions, allowSharing = false }: InteractiveQuizProps) {
const [userAnswers, setUserAnswers] = useState<Record<number, number>>({});
const [isSubmitted, setIsSubmitted] = useState(false);
const [shareUrl, setShareUrl] = useState<string | null>(null);
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const handleSelect = (questionId: number, optionIndex: number) => {
if (isSubmitted) return;
setUserAnswers(prev => ({
...prev,
[questionId]: optionIndex
}));
};
const calculateScore = () => {
let correct = 0;
questions.forEach(q => {
if (userAnswers[q.id] === q.correctAnswer) {
correct++;
}
});
return correct;
};
const handleSubmit = () => {
if (Object.keys(userAnswers).length < questions.length) {
if (!confirm("还有题目未完成,确定要提交吗?")) return;
}
setIsSubmitted(true);
};
const handleGenerateShareLink = async () => {
if (shareUrl) return;
setIsGeneratingLink(true);
try {
const response = await fetch('/api/quiz', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ questions }),
});
const data = await response.json();
if (data.id) {
const url = `${window.location.origin}/quiz/${data.id}`;
setShareUrl(url);
}
} catch (error) {
alert('生成链接失败,请稍后重试');
console.error(error);
} finally {
setIsGeneratingLink(false);
}
};
const copyToClipboard = () => {
if (!shareUrl) return;
navigator.clipboard.writeText(shareUrl);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};
const score = calculateScore();
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 my-4 w-full max-w-2xl mx-auto overflow-hidden">
<div className="p-6 border-b border-gray-100 flex items-center justify-between bg-white">
<h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
<span>📝</span> 知识库测试
<span className="text-sm font-normal text-gray-500 ml-2">({questions.length} 题)</span>
</h2>
<div className="flex items-center gap-3">
{allowSharing && !shareUrl && (
<button
onClick={(e) => {
e.stopPropagation();
handleGenerateShareLink();
}}
disabled={isGeneratingLink}
className="text-sm px-3 py-1.5 bg-primary-50 text-primary-600 rounded-lg hover:bg-primary-100 transition-colors flex items-center gap-2"
>
{isGeneratingLink ? (
<span>生成中...</span>
) : (
<>
<Share2 className="w-4 h-4" />
<span>分享</span>
</>
)}
</button>
)}
</div>
</div>
<div className="p-6 animate-in slide-in-from-top-2 duration-200">
{allowSharing && shareUrl && (
<div className="mb-6 bg-primary-50 border border-primary-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-bold text-primary-800">✅ 试卷链接已生成</span>
<a
href={shareUrl}
target="_blank"
rel="noopener noreferrer"
className="text-xs flex items-center gap-1 text-primary-700 hover:text-primary-900 font-medium bg-white px-2 py-1 rounded border border-primary-200 hover:border-primary-300"
>
<Share2 className="w-3 h-3" />
直接打开
</a>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 text-xs text-gray-500 break-all bg-white p-2 rounded border border-primary-100 select-all">
{shareUrl}
</div>
<button
onClick={copyToClipboard}
className="shrink-0 text-xs flex items-center gap-1 text-primary-700 hover:text-primary-900 font-medium bg-white px-2 py-2 rounded border border-primary-200 hover:border-primary-300"
>
{isCopied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
{isCopied ? "已复制" : "复制"}
</button>
</div>
<p className="text-xs text-primary-700 mt-2">
将此链接发送给员工,他们可以直接在线答题。
</p>
</div>
)}
<div className="space-y-8">
{questions.map((q, index) => (
<div key={q.id} className="border-b border-gray-100 pb-6 last:border-0 last:pb-0">
<h3 className="text-lg font-medium text-gray-900 mb-4 leading-relaxed">
{index + 1}. {q.question}
</h3>
<div className="space-y-3">
{q.options.map((option, optIndex) => {
const isSelected = userAnswers[q.id] === optIndex;
const isCorrect = q.correctAnswer === optIndex;
const isWrong = isSelected && !isCorrect;
let containerClass = "p-3 rounded-lg border cursor-pointer transition-all flex items-center justify-between group ";
if (isSubmitted) {
if (isCorrect) containerClass += "bg-green-50 border-green-200 text-green-900";
else if (isWrong) containerClass += "bg-red-50 border-red-200 text-red-900";
else containerClass += "bg-gray-50 border-gray-200 text-gray-400 opacity-70";
} else {
if (isSelected) containerClass += "bg-primary-50 border-primary-500 text-primary-700 shadow-sm";
else containerClass += "bg-white border-gray-200 hover:bg-gray-50 hover:border-gray-300 text-gray-700";
}
return (
<div
key={optIndex}
onClick={() => handleSelect(q.id, optIndex)}
className={containerClass}
>
<div className="flex items-center gap-3">
<div className={`w-6 h-6 rounded-full border flex items-center justify-center shrink-0 transition-colors ${
isSelected || (isSubmitted && isCorrect)
? "border-current"
: "border-gray-300 group-hover:border-gray-400"
}`}>
{(isSelected || (isSubmitted && isCorrect)) && (
<div className="w-3 h-3 rounded-full bg-current" />
)}
</div>
<span className="text-sm font-medium">{String.fromCharCode(65 + optIndex)}. {option.replace(/^[A-Z][.、]\s*/, '')}</span>
</div>
{isSubmitted && (
<div>
{isCorrect && <CheckCircle className="w-5 h-5 text-green-600" />}
{isWrong && <XCircle className="w-5 h-5 text-red-600" />}
</div>
)}
</div>
);
})}
</div>
{isSubmitted && (
<div className="mt-4 bg-blue-50 p-4 rounded-lg text-sm text-blue-800 flex gap-2 items-start animate-in fade-in slide-in-from-top-2">
<AlertCircle className="w-5 h-5 shrink-0 mt-0.5" />
<div>
<span className="font-bold block mb-1">解析:</span>
{q.explanation}
</div>
</div>
)}
</div>
))}
</div>
{!isSubmitted ? (
<button
onClick={handleSubmit}
className="w-full mt-8 py-3 px-6 bg-gradient-to-r from-primary-600 to-primary-700 hover:from-primary-700 hover:to-primary-800 text-white font-bold rounded-lg shadow-md transition-all transform active:scale-[0.98]"
>
提交答案
</button>
) : (
<div className="mt-8 pt-6 border-t border-gray-200 text-center animate-in zoom-in-95 duration-300">
<div className="text-3xl font-bold text-gray-900 mb-2">
得分: <span className={`
${score === questions.length ? 'text-green-600' : ''}
${score < questions.length && score > 0 ? 'text-blue-600' : ''}
${score === 0 ? 'text-red-600' : ''}
`}>{score}</span> / {questions.length}
</div>
<p className="text-gray-500 font-medium">
{score === questions.length ? "太棒了!全对!🎉" :
score >= questions.length * 0.6 ? "成绩不错,继续加油!💪" : "再接再厉,多复习一下知识库哦!📚"}
</p>
</div>
)}
</div>
</div>
);
}