'use client' import React, { useState, useEffect, useCallback } from 'react' import { useRouter, useParams } from 'next/navigation' import { Users, Trophy, Clock, Play, Square, RefreshCw, Settings, Upload, Plus, UserMinus, AlertCircle, Timer, TestTube, Award } from 'lucide-react' /* ---------- Enhanced Models ---------- */ interface TestCase { input: string expected_output: string description: string is_public: boolean points: number } interface Example { input: string expected_output: string description: string } interface Question { id: string title: string description: string difficulty: 'easy' | 'medium' | 'hard' function_name: string starter_code: Record test_cases: TestCase[] examples: Example[] constraints: string[] time_limit?: number memory_limit?: string correct_solution: Record scoring_method: string total_points: number } interface ExamInfo { title: string exam_code: string status: 'waiting' | 'active' | 'completed' duration_minutes: number participants_count: number max_participants: number problem_title: string problem_description?: string languages: string[] created_at: string host_name: string start_time?: string end_time?: string problem?: Question } interface Participant { name: string score: number completed: boolean joined_at: string submitted_at?: string passed_tests?: number total_tests?: number points_earned?: number total_points?: number language?: string rank?: number } interface LeaderboardData { leaderboard: Participant[] waiting_participants: Participant[] stats: { total_participants: number completed_submissions: number waiting_submissions: number average_score: number highest_score: number } } /* ---------- Enhanced Host Panel Component ---------- */ export default function EnhancedHostPanel() { const params = useParams() const router = useRouter() const examCode = params.examCode as string /* ------- Global state ------- */ const [examInfo, setExamInfo] = useState(null) const [leaderboardData, setLeaderboardData] = useState({ leaderboard: [], waiting_participants: [], stats: { total_participants: 0, completed_submissions: 0, waiting_submissions: 0, average_score: 0, highest_score: 0 } }) const [loading, setLoading] = useState(true) const [error, setError] = useState('') /* ------- UI state ------- */ const [activeTab, setActiveTab] = useState<'overview' | 'participants' | 'questions'>('overview') const [showUploader, setShowUploader] = useState(false) const [showDurationEdit, setShowDurationEdit] = useState(false) const [customDuration, setCustomDuration] = useState(30) /* ------- Enhanced Question draft ------- */ const blankQuestion: Question = { id: '', title: '', description: '', difficulty: 'medium', function_name: 'solve', starter_code: { python: 'def solve():\n # Write your solution here\n pass', java: 'public class Solution {\n public void solve() {\n // Write your solution here\n }\n}', javascript: 'function solve() {\n // Write your solution here\n}' }, test_cases: [{ input: '', expected_output: '', description: 'Test case 1', is_public: true, points: 100 }], examples: [{ input: '', expected_output: '', description: 'Example 1' }], constraints: [''], time_limit: 1000, memory_limit: '128MB', correct_solution: { python: '', java: '', javascript: '' }, scoring_method: 'test_cases', total_points: 100 } const [draft, setDraft] = useState({ ...blankQuestion }) /* ------------------------------------------------------------------- */ /* FIXED EVENT HANDLERS */ /* ------------------------------------------------------------------- */ // ✅ FIXED: Stable event handlers using useCallback const handleTitleChange = useCallback((e: React.ChangeEvent) => { setDraft(prev => ({...prev, title: e.target.value})) }, []) const handleDescriptionChange = useCallback((e: React.ChangeEvent) => { setDraft(prev => ({...prev, description: e.target.value})) }, []) const handleDifficultyChange = useCallback((e: React.ChangeEvent) => { setDraft(prev => ({...prev, difficulty: e.target.value as any})) }, []) const handleTotalPointsChange = useCallback((e: React.ChangeEvent) => { const newTotal = parseInt(e.target.value) || 100 setDraft(prev => ({...prev, total_points: newTotal})) }, []) const handleCorrectSolutionChange = useCallback((e: React.ChangeEvent) => { setDraft(prev => ({ ...prev, correct_solution: {...prev.correct_solution, python: e.target.value} })) }, []) const handleExampleChange = useCallback((index: number, field: keyof Example, value: string) => { setDraft(prev => ({ ...prev, examples: prev.examples.map((ex, i) => i === index ? {...ex, [field]: value} : ex ) })) }, []) /* ------------------------------------------------------------------- */ /* API CALLS */ /* ------------------------------------------------------------------- */ const fetchExamInfo = async () => { setLoading(true) try { const res = await fetch(`http://127.0.0.1:5000/api/exam/info/${examCode}`) const data = await res.json() if (data.success) { setExamInfo(data.exam_info) setCustomDuration(data.exam_info.duration_minutes || 30) setError('') } else { setError(data.error || 'Unable to load exam') } } catch (err) { setError('Backend unreachable') console.error('Failed to fetch exam info:', err) } finally { setLoading(false) } } const fetchLeaderboard = async () => { try { const res = await fetch(`http://127.0.0.1:5000/api/exam/leaderboard/${examCode}`) const data = await res.json() if (data.success) { setLeaderboardData({ leaderboard: data.leaderboard || [], waiting_participants: data.waiting_participants || [], stats: data.stats || { total_participants: 0, completed_submissions: 0, waiting_submissions: 0, average_score: 0, highest_score: 0 } }) } } catch (err) { console.error('Failed to fetch leaderboard:', err) } } useEffect(() => { fetchExamInfo() fetchLeaderboard() // Poll leaderboard every 3 seconds for real-time updates const interval = setInterval(fetchLeaderboard, 3000) return () => clearInterval(interval) }, [examCode]) /* ---------- Enhanced Question Upload ---------- */ const uploadQuestion = async () => { if (!draft.title.trim() || !draft.description.trim()) { alert('Title & description required') return } // Validate test cases const validTestCases = draft.test_cases.filter(tc => tc.expected_output.trim() !== '' ) if (validTestCases.length === 0) { alert('At least one test case with expected output is required') return } // Ensure points add up to total const totalTestPoints = validTestCases.reduce((sum, tc) => sum + tc.points, 0) if (totalTestPoints !== draft.total_points) { if (!confirm(`Test case points (${totalTestPoints}) don't equal total points (${draft.total_points}). Continue anyway?`)) { return } } try { const enhancedQuestion = { ...draft, test_cases: validTestCases, id: Date.now().toString(), languages: Object.keys(draft.starter_code).filter(lang => draft.starter_code[lang].trim()) } const res = await fetch('http://127.0.0.1:5000/api/exam/upload-question', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examCode, question: enhancedQuestion }) }) const data = await res.json() if (data.success) { alert(`✅ Enhanced question uploaded with ${validTestCases.length} test cases!\nTotal points: ${draft.total_points}`) setShowUploader(false) setDraft({ ...blankQuestion }) fetchExamInfo() } else { alert(`❌ ${data.error}`) } } catch (err) { console.error('Upload error:', err) alert('❌ Network error') } } /* ---------- Test Case Management ---------- */ const addTestCase = () => { const newPoints = Math.floor(draft.total_points / (draft.test_cases.length + 1)) setDraft(prev => ({ ...prev, test_cases: [...prev.test_cases, { input: '', expected_output: '', description: `Test case ${prev.test_cases.length + 1}`, is_public: false, points: newPoints }] })) } const updateTestCase = useCallback((index: number, field: keyof TestCase, value: any) => { setDraft(prev => ({ ...prev, test_cases: prev.test_cases.map((tc, i) => i === index ? { ...tc, [field]: value } : tc ) })) }, []) const removeTestCase = (index: number) => { if (draft.test_cases.length <= 1) { alert('At least one test case is required') return } setDraft(prev => ({ ...prev, test_cases: prev.test_cases.filter((_, i) => i !== index) })) } // Auto-distribute points when total points change const redistributePoints = () => { const pointsPerTest = Math.floor(draft.total_points / draft.test_cases.length) const remainder = draft.total_points % draft.test_cases.length setDraft(prev => ({ ...prev, test_cases: prev.test_cases.map((tc, index) => ({ ...tc, points: pointsPerTest + (index < remainder ? 1 : 0) })) })) } /* ---------- Exam Control Functions ---------- */ const startExam = async () => { if (!examInfo?.problem_title) { alert('Please upload a question before starting the exam') return } if (!confirm('Start the exam now? Participants will be able to submit solutions.')) return try { const res = await fetch('http://127.0.0.1:5000/api/exam/start-exam', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examCode }) }) const data = await res.json() if (data.success) { alert('✅ Exam started successfully!') fetchExamInfo() } else { alert(`❌ ${data.error}`) } } catch (err) { console.error('Start exam error:', err) alert('❌ Network error') } } const stopExam = async () => { if (!confirm('Stop the exam immediately? This will end the exam for all participants.')) return try { const res = await fetch('http://127.0.0.1:5000/api/exam/stop-exam', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ exam_code: examCode }) }) const data = await res.json() if (data.success) { alert('✅ Exam stopped successfully!') fetchExamInfo() } else { alert(`❌ ${data.error}`) } } catch (err) { console.error('Stop exam error:', err) alert('❌ Network error') } } /* ---------- FIXED Test Case Editor Component ---------- */ const TestCaseEditor = React.memo(({ testCase, index, onUpdate, onRemove, canRemove }: { testCase: TestCase index: number onUpdate: (index: number, field: keyof TestCase, value: any) => void onRemove: (index: number) => void canRemove: boolean }) => { const handleInputChange = useCallback((field: keyof TestCase, value: any) => { onUpdate(index, field, value) }, [index, onUpdate]) return (
Test Case {index + 1}
handleInputChange('points', parseInt(e.target.value) || 0)} className="w-20 p-1 bg-gray-700 rounded text-sm border border-gray-600 focus:ring-2 focus:ring-blue-500 focus:outline-none" placeholder="Points" min="0" autoComplete="off" /> {canRemove && ( )}