From 51e71da2246773da5f32d2ab4c6d1198f29ab443 Mon Sep 17 00:00:00 2001 From: Wonder Date: Sun, 29 Mar 2026 09:37:25 +0800 Subject: [PATCH] =?UTF-8?q?=20=E2=9C=A8Feat:=20=E5=AE=8C=E6=88=90=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=B5=8B=E8=AF=95=E9=97=AE=E7=AD=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/prompt/quiz.txt | 3 +- frontend/src/App.tsx | 37 +++- frontend/src/components/Guodegang.tsx | 6 +- frontend/src/components/Quiz.tsx | 279 ++++++++++++++++++++++++++ 4 files changed, 316 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/Quiz.tsx diff --git a/backend/prompt/quiz.txt b/backend/prompt/quiz.txt index e154d46..0ef6045 100644 --- a/backend/prompt/quiz.txt +++ b/backend/prompt/quiz.txt @@ -2,4 +2,5 @@ 1. 题目类型包括:概念理解、原理分析、应用场景、最佳实践等 2. 难度分布:5道基础题、5道中级题、5道进阶题 3. 每道题都要提供详细的解析说明 -4. 选项之间要有明显的区别,避免模棱两可 \ No newline at end of file +4. 选项之间要有明显的区别,避免模棱两可 +5. 输出选项不需要提供 ABCD 字母 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9c5746d..414f0cd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { ProgressBar, Label, Tabs } from "@heroui/react"; import Card from "./components/Card"; import Guodegang from "./components/Guodegang"; +import Quiz from "./components/Quiz"; import Topic from "./components/Topic"; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -13,15 +14,21 @@ function App() { const [guodegangData, setGuodegangData]: any = useState(null); const [guodegangLoading, setGuodegangLoading] = useState(false); const [guodegangError, setGuodegangError] = useState(null); + const [quizData, setQuizData]: any = useState(null); + const [quizLoading, setQuizLoading] = useState(false); + const [quizError, setQuizError] = useState(null); const fetchData = async (topicToFetch: string) => { setCardLoading(true); setGuodegangLoading(true); + setQuizLoading(true); setCardError(null); setGuodegangError(null); + setQuizError(null); const cardUrl = `http://localhost:8080/card/${encodeURIComponent(topicToFetch)}`; const guodegangUrl = `http://localhost:8080/guodegang/${encodeURIComponent(topicToFetch)}`; + const quizUrl = `http://localhost:8080/quiz/${encodeURIComponent(topicToFetch)}`; const abortController = new AbortController(); const timeoutId = setTimeout(() => abortController.abort(), 300000); @@ -54,7 +61,21 @@ function App() { } }; - Promise.allSettled([fetchCard(), fetchGuodegang()]).finally(() => { + const fetchQuiz = async () => { + try { + const response = await fetch(quizUrl, { signal: abortController.signal }); + if (!response.ok) throw new Error(`Quiz API failed: ${response.status}`); + const result = await response.json(); + setQuizData(result.message); + } catch (error) { + console.error("Error fetching quiz data:", error); + setQuizError("Failed to load quiz data"); + } finally { + setQuizLoading(false); + } + }; + + Promise.allSettled([fetchCard(), fetchGuodegang(), fetchQuiz()]).finally(() => { clearTimeout(timeoutId); }); }; @@ -68,12 +89,12 @@ function App() { const cardLoaded = !cardLoading && !!cardData; const guodegangLoaded = !guodegangLoading && !!guodegangData; + const quizLoaded = !quizLoading && !!quizData; const progress = (() => { if (!topic) return 0; - if (cardLoaded && guodegangLoaded) return 100; - if (cardLoaded || guodegangLoaded) return 50; - return 10; + const loadedCount = [cardLoaded, guodegangLoaded, quizLoaded].filter(Boolean).length; + return Math.round((loadedCount / 3) * 100); })(); return ( @@ -104,6 +125,11 @@ function App() { 🎭 德云社讲解 + + + + 📝 测验问答 + @@ -112,6 +138,9 @@ function App() { + + + diff --git a/frontend/src/components/Guodegang.tsx b/frontend/src/components/Guodegang.tsx index 66ec2a1..2a54608 100644 --- a/frontend/src/components/Guodegang.tsx +++ b/frontend/src/components/Guodegang.tsx @@ -65,10 +65,8 @@ export default function Guodegang({ topic, data, loading, error }: Props) {
- -

- {data!.content} -

+ + {data!.content} diff --git a/frontend/src/components/Quiz.tsx b/frontend/src/components/Quiz.tsx new file mode 100644 index 0000000..6f4af4c --- /dev/null +++ b/frontend/src/components/Quiz.tsx @@ -0,0 +1,279 @@ +import { Button } from "@heroui/react"; +import { useState, useMemo } from "react"; + +interface Questions { + question: string; + options: string[]; + answer: string; + explanation: string; +} + +interface QuizData { + questions: Questions[]; +} + +interface Props { + topic: string; + data: QuizData | null; + loading: boolean; + error: string | null; +} + +interface UserAnswer { + selected: string; + correct: boolean; +} + +export default function Quiz({ topic, data, loading, error }: Props) { + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + const [selectedOption, setSelectedOption] = useState(null); + const [isAnswerSubmitted, setIsAnswerSubmitted] = useState(false); + const [userAnswers, setUserAnswers] = useState>(new Map()); + const [quizComplete, setQuizComplete] = useState(false); + + const currentQuestion = data?.questions?.[currentQuestionIndex]; + const isLastQuestion = data?.questions ? currentQuestionIndex === data.questions.length - 1 : false; + + const correctCount = useMemo(() => { + return Array.from(userAnswers.values()).filter(a => a.correct).length; + }, [userAnswers]); + + const handleOptionSelect = (option: string) => { + if (!isAnswerSubmitted) { + setSelectedOption(option); + } + }; + + const handleSubmit = () => { + if (selectedOption && currentQuestion) { + const isCorrect = selectedOption === currentQuestion.answer; + setUserAnswers(new Map(userAnswers).set(currentQuestionIndex, { + selected: selectedOption, + correct: isCorrect + })); + setIsAnswerSubmitted(true); + } + }; + + const handleNext = () => { + if (isLastQuestion) { + setQuizComplete(true); + } else { + setCurrentQuestionIndex(prev => prev + 1); + setSelectedOption(null); + setIsAnswerSubmitted(false); + } + }; + + const handleRetry = () => { + setCurrentQuestionIndex(0); + setSelectedOption(null); + setIsAnswerSubmitted(false); + setUserAnswers(new Map()); + setQuizComplete(false); + }; + + if (loading) { + return ( +
+
Loading quiz...
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + if (!data || data.questions.length === 0) { + return ( +
+
+
📝
+
测验问答
+
输入主题后,这里将显示相关的测验题目
+
+
+ ); + } + + if (quizComplete) { + const percentage = Math.round((correctCount / data.questions.length) * 100); + + return ( +
+
+

{topic} - Quiz Complete!

+
+
+ +
+
+
{percentage >= 60 ? '🎉' : percentage >= 40 ? '👍' : '💪'}
+
+ {correctCount} / {data.questions.length} +
+
+ {percentage}% +
+

+ {percentage >= 80 ? 'Excellent!' : percentage >= 60 ? 'Good job!' : 'Keep practicing!'} +

+
+
+ +
+

Review Your Answers

+ {data.questions.map((question, index) => { + const userAnswer = userAnswers.get(index); + const isCorrect = userAnswer?.correct ?? false; + const userSelected = userAnswer?.selected; + + return ( +
+
+ + {isCorrect ? '✓' : '✗'} + +
+

Question {index + 1}: {question.question}

+
+
+ Your answer: {userSelected} + {!isCorrect && ( + (Correct: {question.answer}) + )} +
+
+
+ Explanation: {question.explanation} +
+
+
+
+ ); + })} +
+ +
+ +
+
+ ); + } + + if (!currentQuestion) { + return null; + } + + const isCorrect = selectedOption === currentQuestion.answer; + + return ( +
+
+
+

{topic}

+ + Question {currentQuestionIndex + 1} of {data.questions.length} + +
+
+
+
+
+ +
+

+ {currentQuestion.question} +

+ +
+ {currentQuestion.options.map((option, index) => { + const letter = String.fromCharCode(65 + index); + const isSelected = selectedOption === option; + const isCorrectOption = isAnswerSubmitted && option === currentQuestion.answer; + const isWrongUserAnswer = isAnswerSubmitted && isSelected && option !== currentQuestion.answer; + + return ( + + ); + })} +
+ + {!isAnswerSubmitted ? ( +
+ +
+ ) : ( +
+
+ {isCorrect ? '✓ Correct!' : '✗ Incorrect. Correct answer is ' + currentQuestion.answer} +
+ +
+

Explanation:

+

{currentQuestion.explanation}

+
+ +
+ +
+
+ )} +
+
+ ); +}