Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src/app/api/quiz/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,30 @@ export function generatePrompt(
9. Do not combine multiple options into one answer
10. Each option should be a complete, standalone answer
11. MUST provide EXACTLY 4 options - no more, no less

12. For optimization/improvement questions, options should contain actual code solutions
13. When showing code in options, use proper formatting with backticks

Example of INCORRECT format:
❌ options: ["12", "65", "18", "70", "55"] // Wrong - has 5 options
❌ correctAnswer: "12 and 65" // Wrong - combines options

Example of CORRECT format:

Example of CORRECT format for code-based answers:
✓ options: [
"\`const user = { name: 'John', age: Number('30') };\`",
"\`const user = { name: 'John', age: parseInt('30') };\`",
"\`const user = { name: 'John', age: +'30' };\`",
"\`const user = { name: 'John', age: '30' };\`"
]
✓ correctAnswer: "\`const user = { name: 'John', age: Number('30') };\`"

Example of CORRECT format for text-based answers:
✓ options: [
"Age under 12 or over 65",
"Age under 18",
"Age over 21",
"None of the above"
] // Correct - exactly 4 options
✓ correctAnswer: "Age under 12 or over 65" // Correct - matches exactly one option
"Use the Number() function to convert strings to numbers",
"Leave the value as a string",
"Use JSON.stringify() on the entire object",
"Remove the age property entirely"
]
✓ correctAnswer: "Use the Number() function to convert strings to numbers"

The response must be pure JSON only.`;
}
8 changes: 7 additions & 1 deletion src/components/QuizApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function QuizApp() {
isCustomMode,
toggleCustomMode,
setDifficulty,
isWaiting,
} = useQuiz();

const [mounted, setMounted] = useState(false);
Expand All @@ -50,7 +51,12 @@ export default function QuizApp() {
}, [currentQuestion, loadQuestion]);

if (!mounted) return null;
if (loading) return <LoadingCard />;
if (loading)
return (
<LoadingCard
message={isWaiting ? "Please wait a moment..." : "Loading..."}
/>
);
if (error) return <ErrorCard error={error} onRetry={loadQuestion} />;
if (!currentQuestion) return null;

Expand Down
80 changes: 58 additions & 22 deletions src/components/quiz/AnswerOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useMemo } from "react";

interface AnswerOptionsProps {
options: string[];
Expand All @@ -9,13 +12,59 @@ interface AnswerOptionsProps {
onAnswerSelect: (answer: string) => void;
}

const syntaxHighlighterStyle = {
background: "transparent",
padding: "0.5rem",
margin: 0,
fontSize: "0.875rem",
} as const;

export function AnswerOptions({
options,
correctAnswer,
selectedAnswer,
showExplanation,
onAnswerSelect,
}: AnswerOptionsProps) {
const renderOptionContent = useMemo(() => {
const render = (option: string) => {
const codeMatch = option.match(/^`(.+)`$/);
if (codeMatch) {
return (
<SyntaxHighlighter
language="javascript"
style={vscDarkPlus}
customStyle={syntaxHighlighterStyle}
>
{codeMatch[1]}
</SyntaxHighlighter>
);
}
return option;
};
render.displayName = "RenderOptionContent";
return render;
}, []);

const getButtonVariant = (option: string) => {
if (!showExplanation) return "outline";
if (option === correctAnswer) return "default";
if (option === selectedAnswer) return "destructive";
return "outline";
};

const getButtonClassName = (option: string) => {
const baseClasses =
"w-full text-left justify-start p-6 h-auto transition-colors";
if (!showExplanation)
return `${baseClasses} hover:bg-muted bg-background text-foreground border`;
if (option === correctAnswer)
return `${baseClasses} bg-green-600 hover:bg-green-600 text-white font-medium`;
if (option === selectedAnswer)
return `${baseClasses} bg-red-600 text-white border-red-700`;
return `${baseClasses} hover:bg-muted bg-background text-foreground border`;
};

return (
<div className="grid grid-cols-1 gap-3" role="radiogroup">
{options.map((option, index) => (
Expand All @@ -28,31 +77,18 @@ export function AnswerOptions({
<Button
onClick={() => onAnswerSelect(option)}
disabled={showExplanation}
variant={
showExplanation
? option === correctAnswer
? "default"
: option === selectedAnswer
? "destructive"
: "outline"
: "outline"
}
className={`w-full text-left justify-start p-6 h-auto transition-colors
${
showExplanation && option === correctAnswer
? "bg-green-600 hover:bg-green-600 text-white font-medium"
: showExplanation && option === selectedAnswer
? "bg-red-600 text-white border-red-700"
: "hover:bg-muted bg-background text-foreground border"
}`}
variant={getButtonVariant(option)}
className={getButtonClassName(option)}
role="radio"
aria-checked={option === selectedAnswer}
aria-label={option}
aria-label={option.replace(/`/g, "")}
>
<span className="mr-3 text-sm font-medium px-2.5 py-1.5 bg-muted/50 rounded-md">
{String.fromCharCode(65 + index)}
</span>
{option}
<div className="flex items-start gap-3">
<span className="text-sm font-medium px-2.5 py-1.5 bg-muted/50 rounded-md shrink-0">
{String.fromCharCode(65 + index)}
</span>
<div className="flex-1">{renderOptionContent(option)}</div>
</div>
</Button>
</motion.div>
))}
Expand Down
106 changes: 55 additions & 51 deletions src/components/quiz/FeedbackSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getFeedbackMessage, getProgressFeedback } from "@/utils/feedback";
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
import { Question } from "@/types/quiz";
import { Button } from "@/components/ui/button";
import { memo } from "react";

interface FeedbackSectionProps {
selectedAnswer: string | null;
Expand All @@ -17,30 +18,70 @@ interface ExplanationSectionProps {
onNextQuestion: () => void;
}

export function FeedbackSection({
const FeedbackIcon = memo(({ isCorrect }: { isCorrect: boolean }) =>
isCorrect ? (
<CheckCircle2 className="w-6 h-6 text-green-600 mt-1" />
) : (
<XCircle className="w-6 h-6 text-red-600 mt-1" />
)
);
FeedbackIcon.displayName = "FeedbackIcon";

const ExplanationSection = memo(
({
currentQuestion,
selectedAnswer,
onNextQuestion,
}: ExplanationSectionProps) => (
<div className="bg-white p-6 rounded-xl space-y-4 border border-gray-100 shadow-sm">
<div>
<p className="font-semibold text-slate-800 mb-2">
Detailed Explanation:
</p>
<p className="text-slate-700 leading-relaxed">
{currentQuestion?.explanation}
</p>
</div>

{selectedAnswer !== currentQuestion?.correctAnswer && (
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
<p className="font-semibold text-blue-800 mb-1">Pro Tip:</p>
<p className="text-blue-700">
Remember to {currentQuestion?.topic.toLowerCase()} concepts. Try
practicing with similar examples to reinforce your understanding.
</p>
</div>
)}

<Button
onClick={onNextQuestion}
className="w-full bg-indigo-600 text-white hover:bg-indigo-700 transition-all font-medium"
>
<ArrowRight className="w-4 h-4 mr-2" />
Next Question
</Button>
</div>
)
);
ExplanationSection.displayName = "ExplanationSection";

export const FeedbackSection = memo(function FeedbackSection({
selectedAnswer,
currentQuestion,
difficulty,
correctAnswersInRow,
onNextQuestion,
}: FeedbackSectionProps) {
const isCorrect = selectedAnswer === currentQuestion?.correctAnswer;
const feedbackClasses = isCorrect
? "bg-green-50 border border-green-200 text-green-800"
: "bg-red-50 border border-red-200 text-red-800";

return (
<div className="space-y-4">
<div
className={`p-6 rounded-xl ${
isCorrect
? "bg-green-50 border border-green-200 text-green-800"
: "bg-red-50 border border-red-200 text-red-800"
}`}
>
<div className={`p-6 rounded-xl ${feedbackClasses}`}>
<div className="flex items-start gap-3">
{isCorrect ? (
<CheckCircle2 className="w-6 h-6 text-green-600 mt-1" />
) : (
<XCircle className="w-6 h-6 text-red-600 mt-1" />
)}
<FeedbackIcon isCorrect={isCorrect} />
<div className="space-y-2">
<p className="font-semibold text-slate-800">
{getFeedbackMessage(isCorrect, difficulty)}
Expand All @@ -61,41 +102,4 @@ export function FeedbackSection({
/>
</div>
);
}

function ExplanationSection({
currentQuestion,
selectedAnswer,
onNextQuestion,
}: ExplanationSectionProps) {
return (
<div className="bg-white p-6 rounded-xl space-y-4 border border-gray-100 shadow-sm">
<div>
<p className="font-semibold text-slate-800 mb-2">
Detailed Explanation:
</p>
<p className="text-slate-700 leading-relaxed">
{currentQuestion?.explanation}
</p>
</div>

{selectedAnswer !== currentQuestion?.correctAnswer && (
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
<p className="font-semibold text-blue-800 mb-1">Pro Tip:</p>
<p className="text-blue-700">
Remember to {currentQuestion?.topic.toLowerCase()} concepts. Try
practicing with similar examples to reinforce your understanding.
</p>
</div>
)}

<Button
onClick={onNextQuestion}
className="w-full bg-indigo-600 text-white hover:bg-indigo-700 transition-all font-medium"
>
<ArrowRight className="w-4 h-4 mr-2" />
Next Question
</Button>
</div>
);
}
});
20 changes: 10 additions & 10 deletions src/components/quiz/LoadingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Card, CardContent } from "@/components/ui/card";
import { Loader } from "lucide-react";
interface LoadingCardProps {
message?: string;
}

export function LoadingCard() {
export const LoadingCard = ({ message = "Loading..." }: LoadingCardProps) => {
return (
<Card className="w-full max-w-3xl mx-auto">
<CardContent className="flex items-center justify-center p-8">
<Loader className="w-8 h-8 animate-spin" />
<span className="ml-2">Loading question...</span>
</CardContent>
</Card>
<div className="w-full max-w-2xl mx-auto mt-8 p-6 bg-card rounded-xl shadow-lg animate-pulse">
<div className="flex items-center justify-center">
<p className="text-lg text-muted-foreground">{message}</p>
</div>
</div>
);
}
};
Loading