mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
827 lines
33 KiB
TypeScript
827 lines
33 KiB
TypeScript
'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>
|
||
)
|
||
}
|