mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 11:25:49 +00:00
qizz + panel
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Brain, Target, TrendingUp, Clock, Award, Sparkles, ChevronRight } from 'lucide-react'
|
||||
|
||||
interface Question {
|
||||
question_id: string
|
||||
question_text: string
|
||||
choices: {
|
||||
A: string
|
||||
B: string
|
||||
C: string
|
||||
D: string
|
||||
}
|
||||
correct_answer: string
|
||||
difficulty: string
|
||||
category: string
|
||||
}
|
||||
|
||||
interface SessionStats {
|
||||
session_id: string
|
||||
current_difficulty: string
|
||||
total_questions: number
|
||||
total_correct: number
|
||||
overall_accuracy: number
|
||||
consecutive_correct: {
|
||||
easy: number
|
||||
medium: number
|
||||
hard: number
|
||||
}
|
||||
difficulty_breakdown: {
|
||||
[key: string]: {
|
||||
questions: number
|
||||
correct: number
|
||||
accuracy: number
|
||||
}
|
||||
}
|
||||
model_available: boolean
|
||||
}
|
||||
|
||||
export default function AdaptiveQuizPage() {
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string>('')
|
||||
const [sessionStats, setSessionStats] = useState<SessionStats | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [quizStarted, setQuizStarted] = useState(false)
|
||||
const [quizCompleted, setQuizCompleted] = useState(false)
|
||||
const [lastResult, setLastResult] = useState<any>(null)
|
||||
const [showPrediction, setShowPrediction] = useState(false)
|
||||
const [aiPrediction, setAIPrediction] = useState<any>(null)
|
||||
|
||||
const startQuiz = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/adaptive-quiz/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: `user_${Date.now()}`
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setSessionId(data.session_id)
|
||||
setCurrentQuestion(data.question)
|
||||
setSessionStats(data.session_stats)
|
||||
setQuizStarted(true)
|
||||
} else {
|
||||
alert(`Failed to start quiz: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not start quiz')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submitAnswer = async () => {
|
||||
if (!selectedAnswer || !currentQuestion || !sessionId) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/adaptive-quiz/${sessionId}/answer`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
answer: selectedAnswer,
|
||||
question_data: currentQuestion
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setLastResult(data.result)
|
||||
|
||||
if (data.quiz_completed) {
|
||||
setQuizCompleted(true)
|
||||
setSessionStats(data.final_stats)
|
||||
} else {
|
||||
setCurrentQuestion(data.next_question)
|
||||
setSessionStats(data.session_stats)
|
||||
}
|
||||
|
||||
setSelectedAnswer('')
|
||||
setShowPrediction(false)
|
||||
setAIPrediction(null)
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not submit answer')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getAIPrediction = async () => {
|
||||
if (!currentQuestion) return
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/adaptive-quiz/predict', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
question_text: currentQuestion.question_text,
|
||||
choices: currentQuestion.choices
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setAIPrediction(data.prediction)
|
||||
setShowPrediction(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get AI prediction:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'easy': return 'text-green-400 bg-green-900'
|
||||
case 'medium': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'hard': return 'text-red-400 bg-red-900'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
if (!quizStarted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="max-w-2xl mx-auto p-6 text-center">
|
||||
<div className="mb-8">
|
||||
<Brain className="h-16 w-16 text-purple-400 mx-auto mb-4" />
|
||||
<h1 className="text-4xl font-bold mb-4">🧠 Adaptive AI Quiz</h1>
|
||||
<p className="text-gray-400 max-w-lg mx-auto">
|
||||
Experience an intelligent quiz that adapts to your skill level in real-time using our trained CNN model.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<Target className="h-8 w-8 text-blue-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Adaptive Difficulty</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Questions adjust based on your performance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">AI Predictions</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
See how our AI model would answer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<TrendingUp className="h-8 w-8 text-green-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Smart Analytics</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Track performance across difficulty levels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={startQuiz}
|
||||
disabled={loading}
|
||||
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 px-8 py-4 rounded-lg font-semibold flex items-center justify-center space-x-2 mx-auto"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<span>Start Adaptive Quiz</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (quizCompleted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="text-center mb-8">
|
||||
<Award className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
|
||||
<h1 className="text-3xl font-bold mb-2">Quiz Complete! 🎉</h1>
|
||||
<p className="text-gray-400">
|
||||
You've completed the adaptive quiz. Here are your results:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{sessionStats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-gray-800 p-6 rounded-lg text-center">
|
||||
<div className="text-3xl font-bold text-blue-400 mb-2">
|
||||
{sessionStats.total_questions}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Total Questions</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg text-center">
|
||||
<div className="text-3xl font-bold text-green-400 mb-2">
|
||||
{sessionStats.overall_accuracy}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Overall Accuracy</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg text-center">
|
||||
<div className={`text-3xl font-bold mb-2 ${getDifficultyColor(sessionStats.current_difficulty).split(' ')[0]}`}>
|
||||
{sessionStats.current_difficulty}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Final Difficulty</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg text-center">
|
||||
<div className="text-3xl font-bold text-purple-400 mb-2">
|
||||
{sessionStats.total_correct}/{sessionStats.total_questions}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Correct Answers</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionStats && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h3 className="text-xl font-bold mb-4">Performance by Difficulty</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{Object.entries(sessionStats.difficulty_breakdown).map(([difficulty, stats]) => (
|
||||
<div key={difficulty} className="bg-gray-900 p-4 rounded">
|
||||
<div className={`px-2 py-1 rounded text-xs font-medium mb-2 ${getDifficultyColor(difficulty)}`}>
|
||||
{difficulty.toUpperCase()}
|
||||
</div>
|
||||
<div className="text-lg font-bold">{stats.accuracy}%</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{stats.correct}/{stats.questions} questions
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
setQuizStarted(false)
|
||||
setQuizCompleted(false)
|
||||
setSessionId(null)
|
||||
setCurrentQuestion(null)
|
||||
setSessionStats(null)
|
||||
setLastResult(null)
|
||||
}}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-8 py-3 rounded-lg font-semibold"
|
||||
>
|
||||
Take Another Quiz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header with Stats */}
|
||||
{sessionStats && (
|
||||
<div className="bg-gray-800 p-4 rounded-lg mb-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-lg font-bold text-blue-400">
|
||||
{sessionStats.total_questions}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Questions</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-lg font-bold text-green-400">
|
||||
{sessionStats.overall_accuracy}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Accuracy</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={`text-lg font-bold ${getDifficultyColor(sessionStats.current_difficulty).split(' ')[0]}`}>
|
||||
{sessionStats.current_difficulty}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Current Level</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-lg font-bold text-purple-400">
|
||||
{sessionStats.consecutive_correct[sessionStats.current_difficulty]}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Streak</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={`text-sm px-2 py-1 rounded ${sessionStats.model_available ? 'bg-green-900 text-green-400' : 'bg-yellow-900 text-yellow-400'}`}>
|
||||
{sessionStats.model_available ? '🤖 AI Active' : '🔄 Fallback'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Last Result */}
|
||||
{lastResult && (
|
||||
<div className={`p-4 rounded-lg mb-6 border-l-4 ${lastResult.is_correct ? 'bg-green-900 border-green-500' : 'bg-red-900 border-red-500'}`}>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold">
|
||||
{lastResult.is_correct ? '✅ Correct!' : '❌ Incorrect'}
|
||||
</span>
|
||||
{lastResult.difficulty_changed && (
|
||||
<span className="text-sm bg-blue-900 px-2 py-1 rounded">
|
||||
Level: {lastResult.previous_difficulty} → {lastResult.new_difficulty}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm mt-1">{lastResult.explanation}</p>
|
||||
|
||||
{lastResult.llm_prediction && (
|
||||
<div className="mt-2 text-sm bg-black bg-opacity-30 p-2 rounded">
|
||||
🤖 AI predicted: {lastResult.llm_prediction.llm_prediction}
|
||||
{lastResult.llm_agrees ? ' ✅ (Agreed)' : ' ❌ (Disagreed)'}
|
||||
<span className="ml-2 text-gray-400">
|
||||
({lastResult.llm_prediction.confidence * 100}% confidence)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Question */}
|
||||
{currentQuestion && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getDifficultyColor(currentQuestion.difficulty)}`}>
|
||||
{currentQuestion.difficulty.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{currentQuestion.category}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold">
|
||||
{currentQuestion.question_text}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={getAIPrediction}
|
||||
className="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
<span>AI Hint</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* AI Prediction */}
|
||||
{showPrediction && aiPrediction && (
|
||||
<div className="bg-purple-900 bg-opacity-30 border border-purple-600 p-4 rounded mb-4">
|
||||
<h3 className="font-semibold mb-2 flex items-center space-x-2">
|
||||
<Brain className="h-4 w-4" />
|
||||
<span>🤖 AI Prediction</span>
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
AI suggests: <strong>{aiPrediction.llm_prediction}</strong>
|
||||
</p>
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>Confidence: {(aiPrediction.confidence * 100).toFixed(1)}%</span>
|
||||
<span>{aiPrediction.fallback_mode ? '(Fallback mode)' : '(CNN model)'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Answer Choices */}
|
||||
<div className="space-y-3">
|
||||
{Object.entries(currentQuestion.choices).map(([letter, text]) => (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => setSelectedAnswer(letter)}
|
||||
className={`w-full p-4 text-left rounded-lg border transition-colors ${
|
||||
selectedAnswer === letter
|
||||
? 'bg-blue-900 border-blue-500 text-blue-100'
|
||||
: 'bg-gray-700 border-gray-600 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center text-sm font-bold">
|
||||
{letter}
|
||||
</span>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={submitAnswer}
|
||||
disabled={!selectedAnswer || loading}
|
||||
className="mt-6 w-full bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<span>Submit Answer</span>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,826 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Users, Plus, Trash2, Play, Square, Settings, Brain, Crown, Target } from 'lucide-react'
|
||||
|
||||
interface Question {
|
||||
question_id: string
|
||||
question_text: string
|
||||
options: string[]
|
||||
correct_answer: string
|
||||
difficulty: 'easy' | 'medium' | 'hard'
|
||||
points: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface Participant {
|
||||
session_id: string
|
||||
username: string
|
||||
score: number
|
||||
current_difficulty: string
|
||||
total_questions: number
|
||||
correct_answers: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface QuizRoom {
|
||||
room_id: string
|
||||
room_code: string
|
||||
title: string
|
||||
host_name: string
|
||||
is_private: boolean
|
||||
status: string
|
||||
questions: Question[]
|
||||
participants: Participant[]
|
||||
max_participants: number
|
||||
duration_minutes: number
|
||||
participants_count?: number
|
||||
questions_count?: number
|
||||
questions_by_difficulty?: {
|
||||
easy: number
|
||||
medium: number
|
||||
hard: number
|
||||
}
|
||||
}
|
||||
|
||||
export default function QuizHostPanel() {
|
||||
const router = useRouter()
|
||||
const [currentRoom, setCurrentRoom] = useState<QuizRoom | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'setup' | 'questions' | 'participants' | 'live'>('setup')
|
||||
const [showCreateRoom, setShowCreateRoom] = useState(false)
|
||||
const [showAddQuestion, setShowAddQuestion] = useState(false)
|
||||
const [showAIGenerate, setShowAIGenerate] = useState(false)
|
||||
|
||||
// Room creation form
|
||||
const [roomForm, setRoomForm] = useState({
|
||||
host_name: '',
|
||||
room_title: '',
|
||||
is_private: false,
|
||||
max_participants: 50,
|
||||
duration_minutes: 30
|
||||
})
|
||||
|
||||
// Question form
|
||||
const [questionForm, setQuestionForm] = useState({
|
||||
question_text: '',
|
||||
options: ['', '', '', ''],
|
||||
correct_answer: '',
|
||||
difficulty: 'medium' as 'easy' | 'medium' | 'hard',
|
||||
points: 10,
|
||||
explanation: ''
|
||||
})
|
||||
|
||||
// AI generation form
|
||||
const [aiForm, setAiForm] = useState({
|
||||
topic: '',
|
||||
num_easy: 3,
|
||||
num_medium: 3,
|
||||
num_hard: 2
|
||||
})
|
||||
|
||||
const createRoom = async () => {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/create-room', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(roomForm)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Room creation response:', data) // Debug log
|
||||
|
||||
if (data.success) {
|
||||
// Ensure the room has all required properties
|
||||
const room = {
|
||||
...data.room,
|
||||
status: data.room.status || 'waiting',
|
||||
participants: data.room.participants || [],
|
||||
questions: data.room.questions || []
|
||||
}
|
||||
console.log('Room object:', room) // Debug log
|
||||
setCurrentRoom(room)
|
||||
setShowCreateRoom(false)
|
||||
setActiveTab('questions')
|
||||
alert(`🎉 Room created! Code: ${room.room_code}`)
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Room creation error:', error)
|
||||
alert('Network error: Could not create room')
|
||||
}
|
||||
}
|
||||
|
||||
const addQuestion = async () => {
|
||||
if (!currentRoom) return
|
||||
|
||||
if (!questionForm.question_text || questionForm.options.some(opt => !opt.trim()) || !questionForm.correct_answer) {
|
||||
alert('Please fill all question fields')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/add-question`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(questionForm)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// Refresh room data
|
||||
fetchRoomData()
|
||||
setShowAddQuestion(false)
|
||||
setQuestionForm({
|
||||
question_text: '',
|
||||
options: ['', '', '', ''],
|
||||
correct_answer: '',
|
||||
difficulty: 'medium',
|
||||
points: 10,
|
||||
explanation: ''
|
||||
})
|
||||
alert('✅ Question added successfully!')
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not add question')
|
||||
}
|
||||
}
|
||||
|
||||
const generateAIQuestions = async () => {
|
||||
if (!currentRoom) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/generate-ai-questions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(aiForm)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
fetchRoomData()
|
||||
setShowAIGenerate(false)
|
||||
alert(`🤖 Generated ${data.questions.length} AI questions!`)
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not generate questions')
|
||||
}
|
||||
}
|
||||
|
||||
const removeQuestion = async (questionId: string) => {
|
||||
if (!currentRoom || !confirm('Remove this question?')) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/remove-question/${questionId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
fetchRoomData()
|
||||
alert('✅ Question removed')
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not remove question')
|
||||
}
|
||||
}
|
||||
|
||||
const removeParticipant = async (username: string) => {
|
||||
if (!currentRoom || !confirm(`Remove ${username} from the quiz?`)) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/remove-participant/${username}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
fetchRoomData()
|
||||
alert(`✅ Removed ${username}`)
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not remove participant')
|
||||
}
|
||||
}
|
||||
|
||||
const startQuiz = async () => {
|
||||
if (!currentRoom) return
|
||||
|
||||
if (currentRoom.questions.length === 0) {
|
||||
alert('Add questions before starting the quiz!')
|
||||
return
|
||||
}
|
||||
|
||||
if (!confirm('Start the quiz now? Participants will begin answering questions.')) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/start`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
fetchRoomData()
|
||||
setActiveTab('live')
|
||||
alert('🚀 Quiz started!')
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not start quiz')
|
||||
}
|
||||
}
|
||||
|
||||
const endQuiz = async () => {
|
||||
if (!currentRoom || !confirm('End the quiz now?')) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/end`, {
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
fetchRoomData()
|
||||
alert('✅ Quiz ended!')
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not end quiz')
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRoomData = async () => {
|
||||
if (!currentRoom) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/room/${currentRoom.room_code}/info`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setCurrentRoom(data.room)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch room data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for live updates when quiz is active
|
||||
useEffect(() => {
|
||||
if (currentRoom?.status === 'active') {
|
||||
const interval = setInterval(fetchRoomData, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [currentRoom?.status])
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'easy': return 'text-green-400 bg-green-900'
|
||||
case 'medium': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'hard': return 'text-red-400 bg-red-900'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'waiting': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'active': return 'text-green-400 bg-green-900'
|
||||
case 'completed': return 'text-gray-400 bg-gray-700'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
// Safe status getter
|
||||
const roomStatus = currentRoom?.status || 'waiting'
|
||||
|
||||
if (!currentRoom) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="text-center mb-8">
|
||||
<Crown className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
|
||||
<h1 className="text-4xl font-bold mb-4">👑 Quiz Host Panel</h1>
|
||||
<p className="text-gray-400">
|
||||
Create and manage adaptive quizzes with AI-powered questions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg">
|
||||
<h2 className="text-xl font-bold mb-4">Create New Quiz Room</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your name (Host)"
|
||||
value={roomForm.host_name}
|
||||
onChange={(e) => setRoomForm(prev => ({...prev, host_name: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Quiz room title"
|
||||
value={roomForm.room_title}
|
||||
onChange={(e) => setRoomForm(prev => ({...prev, room_title: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={roomForm.is_private}
|
||||
onChange={(e) => setRoomForm(prev => ({...prev, is_private: e.target.checked}))}
|
||||
className="rounded"
|
||||
/>
|
||||
<span>Private Room (requires code)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Max participants"
|
||||
value={roomForm.max_participants}
|
||||
onChange={(e) => setRoomForm(prev => ({...prev, max_participants: parseInt(e.target.value) || 50}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Duration (minutes)"
|
||||
value={roomForm.duration_minutes}
|
||||
onChange={(e) => setRoomForm(prev => ({...prev, duration_minutes: parseInt(e.target.value) || 30}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
min="5"
|
||||
max="180"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={createRoom}
|
||||
disabled={!roomForm.host_name || !roomForm.room_title}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold"
|
||||
>
|
||||
🚀 Create Quiz Room
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-7xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="bg-gray-800 p-4 rounded-lg mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center space-x-2">
|
||||
<Crown className="h-6 w-6 text-yellow-400" />
|
||||
<span>{currentRoom.title}</span>
|
||||
</h1>
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-400 mt-1">
|
||||
<span>Code: <span className="font-bold text-blue-400">{currentRoom.room_code}</span></span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${getStatusColor(roomStatus)}`}>
|
||||
{roomStatus.toUpperCase()}
|
||||
</span>
|
||||
<span>👥 {currentRoom.participants?.length || 0}/{currentRoom.max_participants}</span>
|
||||
<span>❓ {currentRoom.questions?.length || 0} questions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{roomStatus === 'waiting' && (
|
||||
<button
|
||||
onClick={startQuiz}
|
||||
disabled={(currentRoom.questions?.length || 0) === 0}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Start Quiz</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{roomStatus === 'active' && (
|
||||
<button
|
||||
onClick={endQuiz}
|
||||
className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<Square className="h-4 w-4" />
|
||||
<span>End Quiz</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex space-x-1 mb-6">
|
||||
{[
|
||||
{ id: 'questions', label: `Questions (${currentRoom.questions?.length || 0})`, icon: Target },
|
||||
{ id: 'participants', label: `Participants (${currentRoom.participants?.length || 0})`, icon: Users },
|
||||
{ id: 'live', label: 'Live View', icon: Play }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={`px-4 py-2 rounded flex items-center space-x-2 ${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="h-4 w-4" />
|
||||
<span>{tab.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Questions Tab */}
|
||||
{activeTab === 'questions' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold">📝 Question Management</h2>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setShowAIGenerate(true)}
|
||||
className="bg-purple-600 hover:bg-purple-700 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<Brain className="h-4 w-4" />
|
||||
<span>🤖 AI Generate</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddQuestion(true)}
|
||||
className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Question</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Questions by Difficulty */}
|
||||
{['easy', 'medium', 'hard'].map(difficulty => {
|
||||
const difficultyQuestions = (currentRoom.questions || []).filter(q => q.difficulty === difficulty)
|
||||
return (
|
||||
<div key={difficulty} className="mb-6">
|
||||
<h3 className={`text-lg font-semibold mb-3 px-3 py-1 rounded inline-block ${getDifficultyColor(difficulty)}`}>
|
||||
{difficulty.toUpperCase()} ({difficultyQuestions.length} questions)
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{difficultyQuestions.map((question, index) => (
|
||||
<div key={question.question_id} className="bg-gray-800 p-4 rounded-lg">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold mb-2">{question.question_text}</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-400 mb-2">
|
||||
{question.options.map((option, optIndex) => (
|
||||
<span key={optIndex} className={`${option === question.correct_answer ? 'text-green-400 font-semibold' : ''}`}>
|
||||
{String.fromCharCode(65 + optIndex)}) {option}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Points: {question.points} | Correct: {question.correct_answer}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeQuestion(question.question_id)}
|
||||
disabled={roomStatus !== 'waiting'}
|
||||
className="text-red-400 hover:text-red-300 disabled:text-gray-600 ml-4"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{difficultyQuestions.length === 0 && (
|
||||
<div className="text-center py-4 text-gray-500 border-2 border-dashed border-gray-700 rounded-lg">
|
||||
No {difficulty} questions yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Add Question Modal */}
|
||||
{showAddQuestion && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 p-6 rounded-lg max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-xl font-bold mb-4">➕ Add New Question</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
placeholder="Question text"
|
||||
value={questionForm.question_text}
|
||||
onChange={(e) => setQuestionForm(prev => ({...prev, question_text: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Options:</label>
|
||||
{questionForm.options.map((option, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
placeholder={`Option ${String.fromCharCode(65 + index)}`}
|
||||
value={option}
|
||||
onChange={(e) => {
|
||||
const newOptions = [...questionForm.options]
|
||||
newOptions[index] = e.target.value
|
||||
setQuestionForm(prev => ({...prev, options: newOptions}))
|
||||
}}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Correct answer"
|
||||
value={questionForm.correct_answer}
|
||||
onChange={(e) => setQuestionForm(prev => ({...prev, correct_answer: e.target.value}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<select
|
||||
value={questionForm.difficulty}
|
||||
onChange={(e) => setQuestionForm(prev => ({...prev, difficulty: e.target.value as any}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="easy">🟢 Easy</option>
|
||||
<option value="medium">🟡 Medium</option>
|
||||
<option value="hard">🔴 Hard</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Points"
|
||||
value={questionForm.points}
|
||||
onChange={(e) => setQuestionForm(prev => ({...prev, points: parseInt(e.target.value) || 10}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
placeholder="Explanation (optional)"
|
||||
value={questionForm.explanation}
|
||||
onChange={(e) => setQuestionForm(prev => ({...prev, explanation: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<button
|
||||
onClick={addQuestion}
|
||||
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-semibold"
|
||||
>
|
||||
✅ Add Question
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddQuestion(false)}
|
||||
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Generate Modal */}
|
||||
{showAIGenerate && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-gray-800 p-6 rounded-lg max-w-md w-full mx-4">
|
||||
<h3 className="text-xl font-bold mb-4 flex items-center space-x-2">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
<span>🤖 AI Question Generator</span>
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Topic (e.g., Programming, Science)"
|
||||
value={aiForm.topic}
|
||||
onChange={(e) => setAiForm(prev => ({...prev, topic: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">🟢 Easy</label>
|
||||
<input
|
||||
type="number"
|
||||
value={aiForm.num_easy}
|
||||
onChange={(e) => setAiForm(prev => ({...prev, num_easy: parseInt(e.target.value) || 0}))}
|
||||
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">🟡 Medium</label>
|
||||
<input
|
||||
type="number"
|
||||
value={aiForm.num_medium}
|
||||
onChange={(e) => setAiForm(prev => ({...prev, num_medium: parseInt(e.target.value) || 0}))}
|
||||
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">🔴 Hard</label>
|
||||
<input
|
||||
type="number"
|
||||
value={aiForm.num_hard}
|
||||
onChange={(e) => setAiForm(prev => ({...prev, num_hard: parseInt(e.target.value) || 0}))}
|
||||
className="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4 mt-6">
|
||||
<button
|
||||
onClick={generateAIQuestions}
|
||||
disabled={aiForm.num_easy + aiForm.num_medium + aiForm.num_hard === 0}
|
||||
className="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 px-6 py-2 rounded font-semibold"
|
||||
>
|
||||
🚀 Generate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAIGenerate(false)}
|
||||
className="bg-gray-600 hover:bg-gray-700 px-6 py-2 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Participants Tab */}
|
||||
{activeTab === 'participants' && (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-6">👥 Participant Management</h2>
|
||||
|
||||
{(currentRoom.participants?.length || 0) === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Users className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-xl mb-2">No participants yet</p>
|
||||
<p>Share room code: <span className="font-bold text-blue-400">{currentRoom.room_code}</span></p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{(currentRoom.participants || []).map((participant) => (
|
||||
<div key={participant.session_id} className="bg-gray-800 p-4 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold">{participant.username}</h3>
|
||||
<div className="text-sm text-gray-400">
|
||||
Score: {participant.score} pts
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeParticipant(participant.username)}
|
||||
disabled={roomStatus === 'active'}
|
||||
className="text-red-400 hover:text-red-300 disabled:text-gray-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span>Difficulty:</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${getDifficultyColor(participant.current_difficulty)}`}>
|
||||
{participant.current_difficulty}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Progress:</span>
|
||||
<span>{participant.correct_answers}/{participant.total_questions}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Accuracy:</span>
|
||||
<span>
|
||||
{participant.total_questions > 0
|
||||
? Math.round((participant.correct_answers / participant.total_questions) * 100)
|
||||
: 0}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Live View Tab */}
|
||||
{activeTab === 'live' && (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-6">📺 Live Quiz Dashboard</h2>
|
||||
|
||||
{roomStatus !== 'active' ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Play className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-xl mb-2">Quiz not active</p>
|
||||
<p>Start the quiz to see live updates</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Real-time Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-blue-400">{currentRoom.participants?.length || 0}</div>
|
||||
<div className="text-sm text-gray-400">Active Participants</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-green-400">
|
||||
{Math.round((currentRoom.participants || []).reduce((sum, p) => sum + (p.total_questions > 0 ? (p.correct_answers / p.total_questions) * 100 : 0), 0) / Math.max((currentRoom.participants || []).length, 1))}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Avg Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-purple-400">
|
||||
{Math.max(...(currentRoom.participants || []).map(p => p.score), 0)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Top Score</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<div className="text-2xl font-bold text-yellow-400">
|
||||
{(currentRoom.participants || []).filter(p => p.current_difficulty === 'hard').length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">Hard Level</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Leaderboard */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg">
|
||||
<h3 className="text-lg font-bold mb-4">🏆 Live Leaderboard</h3>
|
||||
<div className="space-y-2">
|
||||
{(currentRoom.participants || [])
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map((participant, index) => (
|
||||
<div key={participant.session_id} className="flex items-center justify-between p-3 bg-gray-700 rounded">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="font-bold text-yellow-400">#{index + 1}</span>
|
||||
<span className="font-semibold">{participant.username}</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${getDifficultyColor(participant.current_difficulty)}`}>
|
||||
{participant.current_difficulty}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-bold">{participant.score} pts</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{participant.correct_answers}/{participant.total_questions} correct
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Users, Lock, Globe, Search, Play } from 'lucide-react'
|
||||
|
||||
interface PublicRoom {
|
||||
room_id: string
|
||||
room_code: string
|
||||
title: string
|
||||
host_name: string
|
||||
participants_count: number
|
||||
max_participants: number
|
||||
questions_count: number
|
||||
status: string
|
||||
}
|
||||
|
||||
export default function QuizJoinPage() {
|
||||
const [joinMode, setJoinMode] = useState<'code' | 'public'>('public')
|
||||
const [roomCode, setRoomCode] = useState('')
|
||||
const [username, setUsername] = useState('')
|
||||
const [publicRooms, setPublicRooms] = useState<PublicRoom[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (joinMode === 'public') {
|
||||
fetchPublicRooms()
|
||||
}
|
||||
}, [joinMode])
|
||||
|
||||
const fetchPublicRooms = async () => {
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/public-rooms')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setPublicRooms(data.public_rooms)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch public rooms:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const joinRoom = async (code: string) => {
|
||||
if (!username.trim()) {
|
||||
alert('Please enter your username')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/join-room', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
room_code: code,
|
||||
username: username.trim()
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
// Store session info and redirect to quiz
|
||||
localStorage.setItem('quiz_session', JSON.stringify(data.session))
|
||||
router.push(`/quiz-play/${data.session.session_id}`)
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not join room')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const joinWithCode = () => {
|
||||
if (!roomCode.trim()) {
|
||||
alert('Please enter room code')
|
||||
return
|
||||
}
|
||||
joinRoom(roomCode.trim().toUpperCase())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="text-center mb-8">
|
||||
<Users className="h-16 w-16 text-blue-400 mx-auto mb-4" />
|
||||
<h1 className="text-4xl font-bold mb-4">🎯 Join Quiz</h1>
|
||||
<p className="text-gray-400">
|
||||
Join an adaptive quiz and test your knowledge!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Username Input */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-bold mb-4">👤 Enter Your Name</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Join Mode Toggle */}
|
||||
<div className="flex space-x-1 mb-6">
|
||||
<button
|
||||
onClick={() => setJoinMode('public')}
|
||||
className={`flex-1 p-4 rounded-lg flex items-center justify-center space-x-2 ${
|
||||
joinMode === 'public'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Globe className="h-5 w-5" />
|
||||
<span>Public Rooms</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setJoinMode('code')}
|
||||
className={`flex-1 p-4 rounded-lg flex items-center justify-center space-x-2 ${
|
||||
joinMode === 'code'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<Lock className="h-5 w-5" />
|
||||
<span>Private Code</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Join with Code */}
|
||||
{joinMode === 'code' && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg">
|
||||
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
|
||||
<Lock className="h-5 w-5 text-yellow-400" />
|
||||
<span>🔐 Join with Room Code</span>
|
||||
</h2>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter room code (e.g., ABC123)"
|
||||
value={roomCode}
|
||||
onChange={(e) => setRoomCode(e.target.value.toUpperCase())}
|
||||
className="flex-1 p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
maxLength={6}
|
||||
/>
|
||||
<button
|
||||
onClick={joinWithCode}
|
||||
disabled={!username.trim() || !roomCode.trim() || loading}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-6 py-3 rounded font-semibold flex items-center space-x-2"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Join</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Public Rooms */}
|
||||
{joinMode === 'public' && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-bold flex items-center space-x-2">
|
||||
<Globe className="h-5 w-5 text-green-400" />
|
||||
<span>🌍 Public Quiz Rooms</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchPublicRooms}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{publicRooms.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Globe className="h-16 w-16 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-xl mb-2">No public rooms available</p>
|
||||
<p>Create your own room or join with a private code</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{publicRooms.map((room) => (
|
||||
<div key={room.room_id} className="bg-gray-700 p-4 rounded-lg hover:bg-gray-650 transition-colors">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{room.title}</h3>
|
||||
<p className="text-sm text-gray-400">Host: {room.host_name}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs ${
|
||||
room.status === 'waiting' ? 'bg-yellow-900 text-yellow-400' : 'bg-green-900 text-green-400'
|
||||
}`}>
|
||||
{room.status.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center text-sm text-gray-400 mb-4">
|
||||
<span>👥 {room.participants_count}/{room.max_participants}</span>
|
||||
<span>❓ {room.questions_count} questions</span>
|
||||
<span>🔢 {room.room_code}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => joinRoom(room.room_code)}
|
||||
disabled={!username.trim() || loading || room.participants_count >= room.max_participants}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 p-3 rounded font-semibold flex items-center justify-center space-x-2"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Join Quiz</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Brain, Trophy, Target, ArrowRight, CheckCircle, XCircle } from 'lucide-react'
|
||||
|
||||
interface Question {
|
||||
question_id: string
|
||||
question_text: string
|
||||
options: string[]
|
||||
correct_answer: string
|
||||
difficulty: string
|
||||
points: number
|
||||
explanation: string
|
||||
}
|
||||
|
||||
interface SessionStats {
|
||||
current_difficulty?: string
|
||||
consecutive_correct?: {
|
||||
easy: number
|
||||
medium: number
|
||||
hard: number
|
||||
}
|
||||
total_questions?: number
|
||||
correct_answers?: number
|
||||
score?: number
|
||||
accuracy?: number
|
||||
}
|
||||
|
||||
export default function QuizPlayPage() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const sessionId = params.sessionId as string
|
||||
|
||||
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null)
|
||||
const [sessionStats, setSessionStats] = useState<SessionStats>({
|
||||
current_difficulty: 'easy',
|
||||
consecutive_correct: { easy: 0, medium: 0, hard: 0 },
|
||||
total_questions: 0,
|
||||
correct_answers: 0,
|
||||
score: 0,
|
||||
accuracy: 0
|
||||
})
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string>('')
|
||||
const [showResult, setShowResult] = useState(false)
|
||||
const [lastResult, setLastResult] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [quizCompleted, setQuizCompleted] = useState(false)
|
||||
|
||||
// ✅ Safe getter for current difficulty with fallback
|
||||
const getCurrentDifficulty = () => {
|
||||
return sessionStats?.current_difficulty || 'easy'
|
||||
}
|
||||
|
||||
// ✅ Safe getter for consecutive correct with fallback
|
||||
const getConsecutiveCorrect = () => {
|
||||
return sessionStats?.consecutive_correct || { easy: 0, medium: 0, hard: 0 }
|
||||
}
|
||||
|
||||
// Fetch next question
|
||||
const fetchNextQuestion = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/session/${sessionId}/next-question`)
|
||||
const data = await response.json()
|
||||
|
||||
console.log('Next question response:', data) // ✅ Debug log
|
||||
|
||||
if (data.success) {
|
||||
if (data.quiz_completed) {
|
||||
setQuizCompleted(true)
|
||||
setCurrentQuestion(null)
|
||||
} else {
|
||||
setCurrentQuestion(data.question)
|
||||
// ✅ Safely update session stats with fallbacks
|
||||
setSessionStats(prev => ({
|
||||
current_difficulty: data.session_stats?.current_difficulty || prev.current_difficulty || 'easy',
|
||||
consecutive_correct: data.session_stats?.consecutive_correct || prev.consecutive_correct || { easy: 0, medium: 0, hard: 0 },
|
||||
total_questions: data.session_stats?.total_questions || prev.total_questions || 0,
|
||||
correct_answers: data.session_stats?.correct_answers || prev.correct_answers || 0,
|
||||
score: data.session_stats?.score || prev.score || 0,
|
||||
accuracy: data.session_stats?.accuracy || prev.accuracy || 0
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch question error:', error)
|
||||
alert('Failed to fetch next question')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Submit answer
|
||||
const submitAnswer = async () => {
|
||||
if (!selectedAnswer || !currentQuestion) return
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/session/${sessionId}/submit-answer`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
answer: selectedAnswer,
|
||||
question_data: currentQuestion
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
console.log('Submit answer response:', data) // ✅ Debug log
|
||||
|
||||
if (data.success) {
|
||||
setLastResult(data)
|
||||
setShowResult(true)
|
||||
|
||||
// ✅ Safely update session stats with fallbacks
|
||||
setSessionStats(prev => ({
|
||||
current_difficulty: data.session_stats?.current_difficulty || prev.current_difficulty || 'easy',
|
||||
consecutive_correct: data.session_stats?.consecutive_correct || prev.consecutive_correct || { easy: 0, medium: 0, hard: 0 },
|
||||
total_questions: data.session_stats?.total_questions || prev.total_questions || 0,
|
||||
correct_answers: data.session_stats?.correct_answers || prev.correct_answers || 0,
|
||||
score: data.session_stats?.score || prev.score || 0,
|
||||
accuracy: data.session_stats?.accuracy || prev.accuracy || 0
|
||||
}))
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit answer error:', error)
|
||||
alert('Failed to submit answer')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next question
|
||||
const continueToNext = () => {
|
||||
setShowResult(false)
|
||||
setSelectedAnswer('')
|
||||
setLastResult(null)
|
||||
fetchNextQuestion()
|
||||
}
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
fetchNextQuestion()
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case 'easy': return 'text-green-400 bg-green-900'
|
||||
case 'medium': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'hard': return 'text-red-400 bg-red-900'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && !currentQuestion) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
||||
<p>Loading question...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (quizCompleted) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="max-w-2xl mx-auto p-6 text-center">
|
||||
<Trophy className="h-16 w-16 text-yellow-400 mx-auto mb-4" />
|
||||
<h1 className="text-3xl font-bold mb-4">🎉 Quiz Completed!</h1>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-bold mb-4">Final Results</h2>
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div className="bg-gray-700 p-4 rounded">
|
||||
<div className="text-2xl font-bold text-blue-400">{sessionStats.score || 0}</div>
|
||||
<div className="text-gray-400">Final Score</div>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-4 rounded">
|
||||
<div className="text-2xl font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
|
||||
<div className="text-gray-400">Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-4 rounded">
|
||||
<div className="text-2xl font-bold text-purple-400">{sessionStats.total_questions || 0}</div>
|
||||
<div className="text-gray-400">Questions</div>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-4 rounded">
|
||||
<div className={`text-2xl font-bold px-3 py-1 rounded ${getDifficultyColor(getCurrentDifficulty())}`}>
|
||||
{getCurrentDifficulty().toUpperCase()}
|
||||
</div>
|
||||
<div className="text-gray-400">Final Level</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/quizzes')}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-lg font-semibold"
|
||||
>
|
||||
Back to Quizzes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (showResult && lastResult) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="max-w-2xl mx-auto p-6">
|
||||
<div className="text-center mb-6">
|
||||
{lastResult.is_correct ? (
|
||||
<CheckCircle className="h-16 w-16 text-green-400 mx-auto mb-4" />
|
||||
) : (
|
||||
<XCircle className="h-16 w-16 text-red-400 mx-auto mb-4" />
|
||||
)}
|
||||
<h1 className="text-3xl font-bold mb-2">
|
||||
{lastResult.is_correct ? '✅ Correct!' : '❌ Incorrect'}
|
||||
</h1>
|
||||
<p className="text-gray-400">
|
||||
{lastResult.is_correct ? 'Great job!' : 'Keep trying!'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h3 className="font-semibold mb-2">Correct Answer:</h3>
|
||||
<p className="text-green-400 mb-4">{lastResult.correct_answer}</p>
|
||||
|
||||
{lastResult.explanation && (
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2">Explanation:</h3>
|
||||
<p className="text-gray-300">{lastResult.explanation}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Difficulty Change Notification */}
|
||||
{lastResult.difficulty_changed && (
|
||||
<div className="bg-blue-900 border border-blue-600 p-4 rounded-lg mb-6">
|
||||
<h3 className="font-semibold mb-2">📈 Difficulty Updated!</h3>
|
||||
<p>
|
||||
Moved from <span className={`px-2 py-1 rounded ${getDifficultyColor(lastResult.previous_difficulty)}`}>
|
||||
{lastResult.previous_difficulty}
|
||||
</span> to <span className={`px-2 py-1 rounded ${getDifficultyColor(lastResult.new_difficulty)}`}>
|
||||
{lastResult.new_difficulty}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Session Stats */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h3 className="font-semibold mb-4">📊 Your Progress</h3>
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-xl font-bold text-blue-400">{sessionStats.score || 0}</div>
|
||||
<div className="text-gray-400 text-sm">Score</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
|
||||
<div className="text-gray-400 text-sm">Accuracy</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Feedback */}
|
||||
{lastResult.ai_feedback && (
|
||||
<div className="bg-purple-900 border border-purple-600 p-4 rounded-lg mb-6">
|
||||
<h3 className="font-semibold mb-2 flex items-center space-x-2">
|
||||
<Brain className="h-5 w-5" />
|
||||
<span>🤖 AI Analysis</span>
|
||||
</h3>
|
||||
<p className="text-purple-200">
|
||||
AI predicted: <span className="font-semibold">{lastResult.ai_feedback.ai_prediction}</span>
|
||||
{lastResult.ai_feedback.ai_agrees ? ' ✅ (Agrees with correct answer)' : ' ❌ (Disagrees)'}
|
||||
</p>
|
||||
<p className="text-xs text-purple-300 mt-1">
|
||||
Confidence: {Math.round(lastResult.ai_feedback.ai_confidence * 100)}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={continueToNext}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span>Continue to Next Question</span>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!currentQuestion) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p>No question available</p>
|
||||
<button
|
||||
onClick={() => router.push('/quizzes')}
|
||||
className="mt-4 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded"
|
||||
>
|
||||
Back to Quizzes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header with Stats */}
|
||||
<div className="bg-gray-800 p-4 rounded-lg mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className={`px-3 py-1 rounded font-semibold ${getDifficultyColor(getCurrentDifficulty())}`}>
|
||||
{getCurrentDifficulty().toUpperCase()}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
Question {(sessionStats.total_questions || 0) + 1}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 text-sm">
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-blue-400">{sessionStats.score || 0}</div>
|
||||
<div className="text-gray-400">Score</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-green-400">{sessionStats.accuracy || 0}%</div>
|
||||
<div className="text-gray-400">Accuracy</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="font-bold text-purple-400">{getConsecutiveCorrect()[getCurrentDifficulty()] || 0}</div>
|
||||
<div className="text-gray-400">Streak</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h1 className="text-2xl font-bold mb-6">{currentQuestion.question_text}</h1>
|
||||
|
||||
<div className="space-y-3">
|
||||
{currentQuestion.options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedAnswer(option)}
|
||||
className={`w-full p-4 text-left rounded-lg border-2 transition-colors ${
|
||||
selectedAnswer === option
|
||||
? 'border-blue-500 bg-blue-900'
|
||||
: 'border-gray-600 bg-gray-700 hover:border-gray-500'
|
||||
}`}
|
||||
>
|
||||
<span className="font-semibold mr-3">{String.fromCharCode(65 + index)})</span>
|
||||
{option}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={submitAnswer}
|
||||
disabled={!selectedAnswer || loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<span>Submit Answer</span>
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,332 @@
|
||||
import { QuizRunner } from "@/components/quiz-runner"
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { Brain, Clock, CheckCircle, XCircle, Sparkles, AlertCircle } from 'lucide-react'
|
||||
|
||||
interface QuizPageProps {
|
||||
params: {
|
||||
quizId: string
|
||||
interface Question {
|
||||
id: string
|
||||
question_number: number
|
||||
question_text: string
|
||||
options: string[]
|
||||
correct_answer: string
|
||||
points: number
|
||||
ai_prediction?: any
|
||||
}
|
||||
|
||||
interface Quiz {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
questions: Question[]
|
||||
generated_by?: string
|
||||
total_points: number
|
||||
}
|
||||
|
||||
export default function QuizTaking() {
|
||||
const params = useParams()
|
||||
const router = useRouter()
|
||||
const quizId = params.quizId as string
|
||||
|
||||
const [quiz, setQuiz] = useState<Quiz | null>(null)
|
||||
const [currentQuestion, setCurrentQuestion] = useState(0)
|
||||
const [answers, setAnswers] = useState<Record<string, string>>({})
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [results, setResults] = useState<any>(null)
|
||||
const [showAIHint, setShowAIHint] = useState(false)
|
||||
const [aiPrediction, setAIPrediction] = useState<any>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchQuiz()
|
||||
}, [quizId])
|
||||
|
||||
const fetchQuiz = async () => {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/${quizId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setQuiz(data.quiz)
|
||||
} else {
|
||||
setError(data.error || 'Quiz not found')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load quiz')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function QuizPage({ params }: QuizPageProps) {
|
||||
return <QuizRunner quizId={params.quizId} />
|
||||
const getAIHint = async () => {
|
||||
if (!quiz || !quiz.questions[currentQuestion]) return
|
||||
|
||||
try {
|
||||
setShowAIHint(true)
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/ai-predict', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
question_text: quiz.questions[currentQuestion].question_text
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setAIPrediction(data.prediction)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to get AI hint:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAnswerSelect = (questionId: string, answer: string) => {
|
||||
setAnswers(prev => ({ ...prev, [questionId]: answer }))
|
||||
}
|
||||
|
||||
const submitQuiz = async () => {
|
||||
if (!quiz) return
|
||||
|
||||
const unanswered = quiz.questions.filter(q => !answers[q.id])
|
||||
if (unanswered.length > 0) {
|
||||
if (!confirm(`You have ${unanswered.length} unanswered questions. Submit anyway?`)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:5000/api/quizzes/${quizId}/submit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
answers,
|
||||
participant_name: 'User' // You can get this from auth context
|
||||
})
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
setResults(data.results)
|
||||
} else {
|
||||
setError(data.error || 'Failed to submit quiz')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to submit quiz')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
||||
<p>Loading AI Quiz...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<AlertCircle className="h-12 w-12 text-red-400 mx-auto mb-4" />
|
||||
<p className="text-xl mb-4">{error}</p>
|
||||
<button
|
||||
onClick={() => router.push('/quizzes')}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
|
||||
>
|
||||
Back to Quizzes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (results) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<div className="text-center mb-8">
|
||||
<div className="text-6xl mb-4">
|
||||
{results.score >= 80 ? '🏆' : results.score >= 60 ? '🎉' : '📚'}
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-2">Quiz Complete!</h1>
|
||||
<p className="text-xl text-gray-300">
|
||||
You scored {results.score}% ({results.correct_answers}/{results.total_questions})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* AI Feedback */}
|
||||
{results.ai_feedback && (
|
||||
<div className="bg-gray-800 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
<span>🤖 AI Feedback</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{results.ai_feedback.map((feedback: any, index: number) => (
|
||||
<div key={index} className="bg-gray-900 p-4 rounded border-l-4 border-purple-500">
|
||||
<h3 className="font-semibold mb-2">Question {index + 1}</h3>
|
||||
<p className="text-sm text-gray-300 mb-2">{feedback.question}</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{feedback.is_correct ? (
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-red-400" />
|
||||
)}
|
||||
<span className="text-sm">
|
||||
Your answer: {feedback.user_answer}
|
||||
</span>
|
||||
</div>
|
||||
{feedback.ai_feedback && (
|
||||
<p className="text-sm text-purple-300 bg-purple-900 bg-opacity-30 p-2 rounded">
|
||||
🤖 {feedback.ai_feedback.feedback}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => router.push('/quizzes')}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-8 py-3 rounded-lg font-semibold"
|
||||
>
|
||||
Back to Quizzes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!quiz) return null
|
||||
|
||||
const question = quiz.questions[currentQuestion]
|
||||
const progress = ((currentQuestion + 1) / quiz.questions.length) * 100
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-bold flex items-center space-x-2">
|
||||
{quiz.generated_by === 'AI' && <Brain className="h-6 w-6 text-purple-400" />}
|
||||
<span>{quiz.title}</span>
|
||||
</h1>
|
||||
<div className="text-sm text-gray-400">
|
||||
Question {currentQuestion + 1} of {quiz.questions.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-600 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Question */}
|
||||
<div className="bg-gray-800 rounded-lg p-6 mb-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{question.question_text}
|
||||
</h2>
|
||||
{quiz.generated_by === 'AI' && (
|
||||
<button
|
||||
onClick={getAIHint}
|
||||
className="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded text-sm flex items-center space-x-1"
|
||||
>
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span>AI Hint</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* AI Hint */}
|
||||
{showAIHint && aiPrediction && (
|
||||
<div className="bg-purple-900 bg-opacity-30 border border-purple-600 p-4 rounded mb-4">
|
||||
<h3 className="font-semibold mb-2 flex items-center space-x-2">
|
||||
<Brain className="h-4 w-4" />
|
||||
<span>🤖 AI Suggestion</span>
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
AI predicts: <strong>{aiPrediction.predicted_answer}</strong>
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Confidence: {(aiPrediction.confidence * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-3">
|
||||
{question.options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleAnswerSelect(question.id, option)}
|
||||
className={`w-full p-4 text-left rounded-lg border transition-colors ${
|
||||
answers[question.id] === option
|
||||
? 'bg-purple-900 border-purple-500 text-purple-100'
|
||||
: 'bg-gray-700 border-gray-600 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="w-6 h-6 rounded-full border-2 border-gray-400 flex items-center justify-center text-sm">
|
||||
{String.fromCharCode(65 + index)}
|
||||
</span>
|
||||
<span>{option}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => setCurrentQuestion(prev => Math.max(0, prev - 1))}
|
||||
disabled={currentQuestion === 0}
|
||||
className="bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 px-6 py-2 rounded"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
{Object.keys(answers).length} of {quiz.questions.length} answered
|
||||
</div>
|
||||
|
||||
{currentQuestion === quiz.questions.length - 1 ? (
|
||||
<button
|
||||
onClick={submitQuiz}
|
||||
disabled={submitting}
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-600 px-6 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
{submitting ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
) : null}
|
||||
<span>{submitting ? 'Submitting...' : 'Submit Quiz'}</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setCurrentQuestion(prev => Math.min(quiz.questions.length - 1, prev + 1))}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Plus, Trash2, Save, ArrowLeft } from 'lucide-react'
|
||||
|
||||
interface Question {
|
||||
question_text: string
|
||||
options: string[]
|
||||
correct_answer: string
|
||||
points: number
|
||||
}
|
||||
|
||||
export default function CreateQuizPage() {
|
||||
const router = useRouter()
|
||||
const [quiz, setQuiz] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
difficulty: 'medium'
|
||||
})
|
||||
const [questions, setQuestions] = useState<Question[]>([])
|
||||
const [currentQuestion, setCurrentQuestion] = useState<Question>({
|
||||
question_text: '',
|
||||
options: ['', '', '', ''],
|
||||
correct_answer: '',
|
||||
points: 10
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const addQuestion = () => {
|
||||
if (!currentQuestion.question_text || currentQuestion.options.some(opt => !opt.trim()) || !currentQuestion.correct_answer) {
|
||||
alert('Please fill all question fields')
|
||||
return
|
||||
}
|
||||
|
||||
setQuestions([...questions, { ...currentQuestion }])
|
||||
setCurrentQuestion({
|
||||
question_text: '',
|
||||
options: ['', '', '', ''],
|
||||
correct_answer: '',
|
||||
points: 10
|
||||
})
|
||||
}
|
||||
|
||||
const removeQuestion = (index: number) => {
|
||||
setQuestions(questions.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const createQuiz = async () => {
|
||||
if (!quiz.title || questions.length === 0) {
|
||||
alert('Please add a title and at least one question')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const quizData = {
|
||||
...quiz,
|
||||
questions: questions.map((q, index) => ({
|
||||
...q,
|
||||
id: `q_${index}`,
|
||||
question_number: index + 1
|
||||
})),
|
||||
total_points: questions.reduce((sum, q) => sum + q.points, 0),
|
||||
created_at: new Date().toISOString(),
|
||||
generated_by: 'manual'
|
||||
}
|
||||
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(quizData)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
alert('✅ Quiz created successfully!')
|
||||
router.push('/quizzes')
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Network error: Could not create quiz')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center space-x-4 mb-8">
|
||||
<button
|
||||
onClick={() => router.push('/quizzes')}
|
||||
className="bg-gray-700 hover:bg-gray-600 p-2 rounded"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</button>
|
||||
<h1 className="text-3xl font-bold">📝 Create New Quiz</h1>
|
||||
</div>
|
||||
|
||||
{/* Quiz Details */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-bold mb-4">Quiz Information</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Quiz title"
|
||||
value={quiz.title}
|
||||
onChange={(e) => setQuiz(prev => ({...prev, title: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Quiz description"
|
||||
value={quiz.description}
|
||||
onChange={(e) => setQuiz(prev => ({...prev, description: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<select
|
||||
value={quiz.difficulty}
|
||||
onChange={(e) => setQuiz(prev => ({...prev, difficulty: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="easy">🟢 Easy</option>
|
||||
<option value="medium">🟡 Medium</option>
|
||||
<option value="hard">🔴 Hard</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add Question */}
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-bold mb-4">Add Question</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
placeholder="Question text"
|
||||
value={currentQuestion.question_text}
|
||||
onChange={(e) => setCurrentQuestion(prev => ({...prev, question_text: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"
|
||||
rows={3}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Options:</label>
|
||||
{currentQuestion.options.map((option, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
placeholder={`Option ${String.fromCharCode(65 + index)}`}
|
||||
value={option}
|
||||
onChange={(e) => {
|
||||
const newOptions = [...currentQuestion.options]
|
||||
newOptions[index] = e.target.value
|
||||
setCurrentQuestion(prev => ({...prev, options: newOptions}))
|
||||
}}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Correct answer"
|
||||
value={currentQuestion.correct_answer}
|
||||
onChange={(e) => setCurrentQuestion(prev => ({...prev, correct_answer: e.target.value}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Points"
|
||||
value={currentQuestion.points}
|
||||
onChange={(e) => setCurrentQuestion(prev => ({...prev, points: parseInt(e.target.value) || 10}))}
|
||||
className="p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={addQuestion}
|
||||
className="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-semibold flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Question</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Questions List */}
|
||||
{questions.length > 0 && (
|
||||
<div className="bg-gray-800 p-6 rounded-lg mb-6">
|
||||
<h2 className="text-xl font-bold mb-4">Questions ({questions.length})</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{questions.map((question, index) => (
|
||||
<div key={index} className="bg-gray-700 p-4 rounded-lg">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold mb-2">Q{index + 1}: {question.question_text}</h3>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm text-gray-400 mb-2">
|
||||
{question.options.map((option, optIndex) => (
|
||||
<span key={optIndex} className={`${option === question.correct_answer ? 'text-green-400 font-semibold' : ''}`}>
|
||||
{String.fromCharCode(65 + optIndex)}) {option}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Points: {question.points} | Correct: {question.correct_answer}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeQuestion(index)}
|
||||
className="text-red-400 hover:text-red-300 ml-4"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Button */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={createQuiz}
|
||||
disabled={loading || !quiz.title || questions.length === 0}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 px-8 py-3 rounded-lg font-semibold flex items-center space-x-2 mx-auto"
|
||||
>
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-5 w-5" />
|
||||
<span>Create Quiz</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Brain, Sparkles, Settings, Clock, Trophy, AlertCircle } from 'lucide-react'
|
||||
|
||||
export default function AIQuizGenerator() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
topic: '',
|
||||
difficulty: 'medium',
|
||||
num_questions: 5
|
||||
})
|
||||
const [generatedQuiz, setGeneratedQuiz] = useState(null)
|
||||
const [error, setError] = useState('')
|
||||
const router = useRouter()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/generate-ai', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setGeneratedQuiz(data.quiz)
|
||||
// Redirect to the generated quiz
|
||||
router.push(`/quizzes/${data.quiz.id}`)
|
||||
} else {
|
||||
setError(data.error || 'Failed to generate quiz')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Network error: Could not generate quiz')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center space-x-3 mb-4">
|
||||
<Brain className="h-12 w-12 text-purple-400" />
|
||||
<Sparkles className="h-8 w-8 text-yellow-400" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-2">🤖 AI Quiz Generator</h1>
|
||||
<p className="text-gray-400">
|
||||
Generate intelligent quizzes using our trained CNN model
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="bg-red-900 border border-red-600 p-4 rounded-lg mb-6 flex items-center space-x-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generator Form */}
|
||||
<div className="bg-gray-800 rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-bold mb-4 flex items-center space-x-2">
|
||||
<Settings className="h-5 w-5 text-blue-400" />
|
||||
<span>Quiz Configuration</span>
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Topic Input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Topic/Subject
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.topic}
|
||||
onChange={(e) => setFormData(prev => ({...prev, topic: e.target.value}))}
|
||||
placeholder="e.g., Science, History, Technology"
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
AI will generate questions related to this topic
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Difficulty Selection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Difficulty Level
|
||||
</label>
|
||||
<select
|
||||
value={formData.difficulty}
|
||||
onChange={(e) => setFormData(prev => ({...prev, difficulty: e.target.value}))}
|
||||
className="w-full p-3 bg-gray-700 rounded border border-gray-600 focus:ring-2 focus:ring-purple-500 focus:outline-none"
|
||||
>
|
||||
<option value="easy">🟢 Easy</option>
|
||||
<option value="medium">🟡 Medium</option>
|
||||
<option value="hard">🔴 Hard</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Number of Questions */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Number of Questions
|
||||
</label>
|
||||
<div className="flex items-center space-x-4">
|
||||
<input
|
||||
type="range"
|
||||
min="3"
|
||||
max="20"
|
||||
value={formData.num_questions}
|
||||
onChange={(e) => setFormData(prev => ({...prev, num_questions: parseInt(e.target.value)}))}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="bg-gray-700 px-3 py-1 rounded font-bold">
|
||||
{formData.num_questions}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Generate Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 disabled:from-gray-600 disabled:to-gray-600 p-4 rounded-lg font-semibold flex items-center justify-center space-x-2 transition-colors"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
||||
<span>Generating Quiz...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Brain className="h-5 w-5" />
|
||||
<span>🚀 Generate AI Quiz</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">AI-Powered</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Uses trained CNN model for intelligent question selection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<Clock className="h-8 w-8 text-blue-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Instant Generation</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Generate quizzes in seconds with AI processing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg text-center">
|
||||
<Trophy className="h-8 w-8 text-yellow-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Smart Feedback</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
AI provides intelligent feedback on answers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,436 @@
|
||||
import { QuizList } from "@/components/quiz-list"
|
||||
'use client'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Brain, Plus, Clock, Trophy, Users, Sparkles, Crown, Target, Play, Globe, Lock } from 'lucide-react'
|
||||
|
||||
interface Quiz {
|
||||
_id: string
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
difficulty: string
|
||||
questions: any[]
|
||||
generated_by?: string
|
||||
created_at: string
|
||||
total_points: number
|
||||
}
|
||||
|
||||
interface QuizRoom {
|
||||
room_id: string
|
||||
room_code: string
|
||||
title: string
|
||||
host_name: string
|
||||
is_private: boolean
|
||||
status: string
|
||||
participants_count: number
|
||||
questions_count: number
|
||||
questions_by_difficulty: {
|
||||
easy: number
|
||||
medium: number
|
||||
hard: number
|
||||
}
|
||||
}
|
||||
|
||||
export default function QuizzesPage() {
|
||||
return <QuizList />
|
||||
const [activeTab, setActiveTab] = useState<'traditional' | 'rooms' | 'adaptive'>('rooms')
|
||||
const [quizzes, setQuizzes] = useState<Quiz[]>([])
|
||||
const [publicRooms, setPublicRooms] = useState<QuizRoom[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [aiAvailable, setAiAvailable] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'traditional') {
|
||||
fetchTraditionalQuizzes()
|
||||
} else if (activeTab === 'rooms') {
|
||||
fetchPublicRooms()
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
const fetchTraditionalQuizzes = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setQuizzes(data.quizzes)
|
||||
setAiAvailable(data.ai_available)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch quizzes:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPublicRooms = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('http://127.0.0.1:5000/api/quizzes/public-rooms')
|
||||
const data = await response.json()
|
||||
|
||||
if (data.success) {
|
||||
setPublicRooms(data.public_rooms)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch public rooms:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getDifficultyColor = (difficulty: string) => {
|
||||
switch (difficulty.toLowerCase()) {
|
||||
case 'easy': return 'text-green-400 bg-green-900'
|
||||
case 'medium': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'hard': return 'text-red-400 bg-red-900'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'waiting': return 'text-yellow-400 bg-yellow-900'
|
||||
case 'active': return 'text-green-400 bg-green-900'
|
||||
case 'completed': return 'text-gray-400 bg-gray-700'
|
||||
default: return 'text-gray-400 bg-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
if (loading && activeTab === 'traditional' && quizzes.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto mb-4"></div>
|
||||
<p>Loading quizzes...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<div className="max-w-7xl mx-auto p-6">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold mb-4 flex items-center justify-center space-x-3">
|
||||
<Trophy className="h-10 w-10 text-yellow-400" />
|
||||
<span>🧠 OpenLearnX Quiz Platform</span>
|
||||
</h1>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
||||
Experience adaptive quizzes with AI-powered questions and real-time difficulty adjustment
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex justify-center space-x-1 mb-8">
|
||||
{[
|
||||
{ id: 'rooms', label: 'Live Quiz Rooms', icon: Users, description: 'Join or host live quizzes' },
|
||||
{ id: 'adaptive', label: 'Adaptive Quiz', icon: Brain, description: 'AI-powered adaptive difficulty' },
|
||||
{ id: 'traditional', label: 'Traditional Quizzes', icon: Target, description: 'Fixed question sets' }
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as any)}
|
||||
className={`px-6 py-3 rounded-lg flex items-center space-x-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="h-5 w-5" />
|
||||
<div className="text-left">
|
||||
<div className="font-semibold">{tab.label}</div>
|
||||
<div className="text-xs opacity-75">{tab.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Live Quiz Rooms Tab */}
|
||||
{activeTab === 'rooms' && (
|
||||
<div>
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-8 justify-center items-center">
|
||||
<button
|
||||
onClick={() => router.push('/quiz-host')}
|
||||
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2 transition-colors"
|
||||
>
|
||||
<Crown className="h-5 w-5" />
|
||||
<span>👑 Host a Quiz</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/quiz-join')}
|
||||
className="bg-green-600 hover:bg-green-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2"
|
||||
>
|
||||
<Users className="h-5 w-5" />
|
||||
<span>🎯 Join Quiz</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Public Rooms Grid */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold flex items-center space-x-2">
|
||||
<Globe className="h-6 w-6 text-green-400" />
|
||||
<span>🌍 Public Quiz Rooms</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={fetchPublicRooms}
|
||||
className="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded flex items-center space-x-2"
|
||||
>
|
||||
<span>🔄 Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p>Loading rooms...</p>
|
||||
</div>
|
||||
) : publicRooms.length === 0 ? (
|
||||
<div className="text-center py-12 bg-gray-800 rounded-lg">
|
||||
<Globe className="h-16 w-16 text-gray-600 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-2">No Public Rooms Available</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Be the first to create a public quiz room!
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/quiz-host')}
|
||||
className="bg-purple-600 hover:bg-purple-700 px-6 py-3 rounded-lg font-semibold"
|
||||
>
|
||||
🚀 Create Room
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{publicRooms.map((room) => (
|
||||
<div
|
||||
key={room.room_id}
|
||||
className="bg-gray-800 rounded-lg p-6 hover:bg-gray-750 transition-colors border border-gray-700"
|
||||
>
|
||||
{/* Room Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold flex items-center space-x-2">
|
||||
<Globe className="h-5 w-5 text-green-400" />
|
||||
<span>{room.title}</span>
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">Host: {room.host_name}</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getStatusColor(room.status)}`}>
|
||||
{room.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Room Stats */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4 text-sm">
|
||||
<div className="bg-gray-700 p-3 rounded text-center">
|
||||
<div className="font-bold text-blue-400">{room.participants_count}</div>
|
||||
<div className="text-gray-400">Participants</div>
|
||||
</div>
|
||||
<div className="bg-gray-700 p-3 rounded text-center">
|
||||
<div className="font-bold text-purple-400">{room.questions_count}</div>
|
||||
<div className="text-gray-400">Questions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Difficulty Breakdown */}
|
||||
<div className="flex justify-between text-xs mb-4">
|
||||
<span className="text-green-400">Easy: {room.questions_by_difficulty?.easy || 0}</span>
|
||||
<span className="text-yellow-400">Medium: {room.questions_by_difficulty?.medium || 0}</span>
|
||||
<span className="text-red-400">Hard: {room.questions_by_difficulty?.hard || 0}</span>
|
||||
</div>
|
||||
|
||||
{/* Room Code */}
|
||||
<div className="text-center mb-4">
|
||||
<span className="bg-gray-700 px-3 py-1 rounded font-mono text-blue-400">
|
||||
Code: {room.room_code}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Join Button */}
|
||||
<button
|
||||
onClick={() => router.push(`/quiz-join?room=${room.room_code}`)}
|
||||
className="w-full bg-green-600 hover:bg-green-700 p-3 rounded font-semibold flex items-center justify-center space-x-2"
|
||||
>
|
||||
<Play className="h-4 w-4" />
|
||||
<span>Join Room</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Adaptive Quiz Tab */}
|
||||
{activeTab === 'adaptive' && (
|
||||
<div className="text-center">
|
||||
<div className="max-w-2xl mx-auto mb-8">
|
||||
<Brain className="h-16 w-16 text-purple-400 mx-auto mb-4" />
|
||||
<h2 className="text-3xl font-bold mb-4">🧠 Adaptive AI Quiz</h2>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Experience an intelligent quiz that adapts to your skill level in real-time using our trained CNN model.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<Target className="h-8 w-8 text-blue-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Adaptive Difficulty</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Questions adjust based on your performance
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<Brain className="h-8 w-8 text-purple-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">AI Predictions</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
See how our AI model would answer
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 p-4 rounded-lg">
|
||||
<Sparkles className="h-8 w-8 text-green-400 mx-auto mb-2" />
|
||||
<h3 className="font-semibold mb-1">Smart Analytics</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
Track performance across difficulty levels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/adaptive-quiz')}
|
||||
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-8 py-4 rounded-lg font-semibold flex items-center justify-center space-x-2 mx-auto"
|
||||
>
|
||||
<Sparkles className="h-5 w-5" />
|
||||
<span>🚀 Start Adaptive Quiz</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Traditional Quizzes Tab */}
|
||||
{activeTab === 'traditional' && (
|
||||
<div>
|
||||
{/* AI Status & Create Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-8 justify-center items-center">
|
||||
{aiAvailable && (
|
||||
<button
|
||||
onClick={() => router.push('/quizzes/generate')}
|
||||
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2 transition-colors"
|
||||
>
|
||||
<Brain className="h-5 w-5" />
|
||||
<Sparkles className="h-4 w-4" />
|
||||
<span>🚀 Generate AI Quiz</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => router.push('/quizzes/create')}
|
||||
className="bg-green-600 hover:bg-green-700 px-6 py-3 rounded-lg font-semibold flex items-center space-x-2"
|
||||
>
|
||||
<Plus className="h-5 w-5" />
|
||||
<span>Create Manual Quiz</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* AI Status Banner */}
|
||||
{aiAvailable && (
|
||||
<div className="bg-gradient-to-r from-purple-900 to-blue-900 border border-purple-600 p-4 rounded-lg mb-8">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Brain className="h-6 w-6 text-purple-400" />
|
||||
<div>
|
||||
<h3 className="font-semibold">🤖 AI Service Active</h3>
|
||||
<p className="text-sm text-gray-300">
|
||||
Our trained CNN model is ready to generate intelligent quizzes and provide feedback
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Traditional Quizzes Grid */}
|
||||
{quizzes.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Brain className="h-16 w-16 text-gray-600 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-semibold mb-2">No Traditional Quizzes Yet</h3>
|
||||
<p className="text-gray-400 mb-6">
|
||||
Create your first quiz or generate one using AI
|
||||
</p>
|
||||
{aiAvailable && (
|
||||
<button
|
||||
onClick={() => router.push('/quizzes/generate')}
|
||||
className="bg-purple-600 hover:bg-purple-700 px-6 py-3 rounded-lg font-semibold"
|
||||
>
|
||||
🚀 Generate AI Quiz
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{quizzes.map((quiz) => (
|
||||
<div
|
||||
key={quiz._id}
|
||||
className="bg-gray-800 rounded-lg p-6 hover:bg-gray-750 transition-colors cursor-pointer"
|
||||
onClick={() => router.push(`/quizzes/${quiz.id}`)}
|
||||
>
|
||||
{/* Quiz Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold flex items-center space-x-2">
|
||||
{quiz.generated_by === 'AI' && (
|
||||
<Brain className="h-5 w-5 text-purple-400" />
|
||||
)}
|
||||
<span>{quiz.title}</span>
|
||||
</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getDifficultyColor(quiz.difficulty)}`}>
|
||||
{quiz.difficulty}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray-400 text-sm mb-4 line-clamp-2">
|
||||
{quiz.description}
|
||||
</p>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="flex items-center space-x-1">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>{quiz.questions?.length || 0} questions</span>
|
||||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<Trophy className="h-4 w-4" />
|
||||
<span>{quiz.total_points} pts</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{quiz.generated_by === 'AI' && (
|
||||
<div className="flex items-center space-x-1 text-purple-400">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
<span className="text-xs">AI Generated</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="mt-3 pt-3 border-t border-gray-700">
|
||||
<span className="text-xs text-gray-500">
|
||||
Created {new Date(quiz.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user