import { View, Exam, Submission } from './types'; import { saveExams, loadExams, saveSubmissions, loadSubmissions, generateId, exportToJSON, importFromJSON } from './store'; import { autoGradeSubmission, formatTime } from './utils'; // App state let currentView: View = 'dashboard'; let editingExamId: string | null = null; let takingExamId: string | null = null; let gradingSubmissionId: string | null = null; let viewingResultsExamId: string | null = null; // DOM const app = document.getElementById('app')!; function render() { app.innerHTML = ''; switch (currentView) { case 'dashboard': renderDashboard(); break; case 'editor': renderEditor(); break; case 'take': renderTake(); break; case 'submissions': renderSubmissions(); break; case 'grade': renderGrade(); break; case 'results': renderResults(); break; case 'export': renderExport(); break; } } function navigate(view: View, params?: { examId?: string; submissionId?: string }) { currentView = view; editingExamId = params?.examId ?? null; takingExamId = params?.examId ?? null; gradingSubmissionId = params?.submissionId ?? null; viewingResultsExamId = params?.examId ?? null; render(); } function renderNav(): string { return ` `; } // Make navigate available globally for inline handlers (window as any).navigate = navigate; function renderDashboard() { const exams = loadExams(); const submissions = loadSubmissions(); app.innerHTML = ` ${renderNav()}

Digital Exams

Create, conduct, and grade exams — all in your browser. No server. No data leaves your device.

${exams.length}
Exams
${submissions.length}
Submissions
${submissions.filter(s => s.graded).length}
Graded

Your Exams

${exams.length === 0 ? `

No exams yet. Create your first exam to get started.

` : `
${exams.map(exam => `

${escapeHtml(exam.title)}

${exam.questions.length} questions

${escapeHtml(exam.subject)}

${escapeHtml(exam.description)}

⏱ ${exam.timeLimit > 0 ? exam.timeLimit + ' min' : 'No limit'} 🔀 ${exam.shuffleQuestions ? 'Shuffled' : 'Fixed order'}
`).join('')}
`}

Recent Submissions

${submissions.length === 0 ? `

No submissions yet. Take an exam to see results here.

` : `
${submissions.slice(0, 10).map(sub => { const exam = exams.find(e => e.id === sub.examId); return `
${escapeHtml(sub.studentName)} ${escapeHtml(exam?.title ?? 'Unknown exam')} ${sub.percentage}% ${sub.graded ? 'Graded' : 'Pending'}
`; }).join('')}
`}
`; } function escapeHtml(text: string): string { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } (window as any).createNewExam = () => { editingExamId = null; navigate('editor'); }; (window as any).editExam = (id: string) => navigate('editor', { examId: id }); (window as any).takeExam = (id: string) => navigate('take', { examId: id }); (window as any).viewResults = (id: string) => navigate('results', { examId: id }); (window as any).gradeSubmission = (id: string) => navigate('grade', { submissionId: id }); (window as any).deleteExam = (id: string) => { if (!confirm('Delete this exam and all its submissions?')) return; const exams = loadExams().filter(e => e.id !== id); const submissions = loadSubmissions().filter(s => s.examId !== id); saveExams(exams); saveSubmissions(submissions); render(); }; function renderEditor() { const exams = loadExams(); const exam = editingExamId ? exams.find(e => e.id === editingExamId) : null; const questions = exam?.questions ?? []; app.innerHTML = ` ${renderNav()}

${exam ? 'Edit Exam' : 'New Exam'}

Questions (${questions.length})

${questions.map((q, i) => renderQuestionEditor(q, i)).join('')}
`; } function renderQuestionEditor(q: any, index: number): string { const optionsHtml = q.options?.map((opt: string, i: number) => `
`).join('') ?? ''; const optionsEditor = q.type === 'multiple_choice' ? `
${optionsHtml}
` : ''; const correctAnswerInput = q.type === 'fill_blank' ? `
` : q.type === 'true_false' ? `
` : ''; return `
${index + 1} ${q.type.replace('_', ' ')}
${optionsEditor} ${correctAnswerInput}
`; } (window as any).addQuestion = (type: string) => { const container = document.getElementById('questions-container')!; const qid = generateId(); const newQ = { id: qid, type, text: '', points: type === 'essay' ? 10 : (type === 'short_answer' ? 5 : 1), options: type === 'multiple_choice' ? ['', ''] : undefined, correctAnswer: type === 'true_false' ? 'true' : undefined, }; const div = document.createElement('div'); div.innerHTML = renderQuestionEditor(newQ, container.children.length); container.appendChild(div.firstElementChild!); }; (window as any).addOption = (qid: string) => { const list = document.getElementById(`options-${qid}`)!; const idx = list.querySelectorAll('.option-row').length; const row = document.createElement('div'); row.className = 'option-row'; row.innerHTML = ` `; list.insertBefore(row, list.lastElementChild); }; (window as any).removeOption = (qid: string, idx: number) => { const list = document.getElementById(`options-${qid}`)!; const rows = list.querySelectorAll('.option-row'); if (rows[idx]) rows[idx].remove(); }; (window as any).removeQuestion = (qid: string) => { const el = document.querySelector(`[data-qid="${qid}"]`); el?.remove(); // Renumber document.querySelectorAll('.question-editor').forEach((el, i) => { el.querySelector('.question-number')!.textContent = String(i + 1); }); }; (window as any).saveExam = () => { const title = (document.getElementById('exam-title') as HTMLInputElement).value.trim(); if (!title) { alert('Title is required'); return; } const exam: Exam = { id: editingExamId ?? generateId(), title, subject: (document.getElementById('exam-subject') as HTMLInputElement).value.trim(), description: (document.getElementById('exam-desc') as HTMLTextAreaElement).value.trim(), timeLimit: parseInt((document.getElementById('exam-time') as HTMLInputElement).value) || 0, shuffleQuestions: (document.getElementById('exam-shuffle') as HTMLInputElement).checked, allowRetake: (document.getElementById('exam-retake') as HTMLInputElement).checked, showResults: (document.getElementById('exam-showresults') as HTMLInputElement).checked, questions: [], createdAt: editingExamId ? loadExams().find(e => e.id === editingExamId)!.createdAt : new Date().toISOString(), updatedAt: new Date().toISOString(), }; // Collect questions from DOM document.querySelectorAll('.question-editor').forEach(el => { const qid = el.getAttribute('data-qid')!; const type = el.querySelector('.question-type-badge')!.textContent!.replace(' ', '_') as any; const text = (el.querySelector(`#text-${qid}`) as HTMLTextAreaElement).value.trim(); const points = parseInt((el.querySelector(`#points-${qid}`) as HTMLInputElement).value) || 0; const q: any = { id: qid, type, text, points }; if (type === 'multiple_choice') { const options: string[] = []; let correctAnswer = ''; el.querySelectorAll('.option-row').forEach((row, i) => { const optText = (row.querySelector('.option-text') as HTMLInputElement)?.value.trim() ?? ''; if (optText) options.push(optText); const radio = row.querySelector('input[type="radio"]') as HTMLInputElement; if (radio?.checked) correctAnswer = String(i); }); q.options = options; q.correctAnswer = correctAnswer; } else if (type === 'true_false') { q.correctAnswer = (el.querySelector(`#correct-${qid}`) as HTMLSelectElement)?.value; } else if (type === 'fill_blank') { const raw = (el.querySelector(`#correct-${qid}`) as HTMLInputElement)?.value.trim() ?? ''; q.correctAnswer = raw ? raw.split(',').map((s: string) => s.trim()).filter(Boolean) : []; q.caseSensitive = (el.querySelector(`#case-${qid}`) as HTMLInputElement)?.checked; } exam.questions.push(q); }); const exams = loadExams().filter(e => e.id !== exam.id); exams.push(exam); saveExams(exams); navigate('dashboard'); }; function renderTake() { const exams = loadExams(); const exam = exams.find(e => e.id === takingExamId); if (!exam) { app.innerHTML = `

Exam not found.

`; return; } const questions = exam.shuffleQuestions ? [...exam.questions].sort(() => Math.random() - 0.5) : exam.questions; app.innerHTML = ` ${renderNav()}

${escapeHtml(exam.title)}

${exam.questions.length} questions ${exam.questions.reduce((a, q) => a + q.points, 0)} points ${exam.timeLimit > 0 ? `⏱ ${formatTime(exam.timeLimit * 60)}` : ''}
${questions.map((q, i) => renderQuestionTake(q, i)).join('')}
`; // Start timer if (exam.timeLimit > 0) { let remaining = exam.timeLimit * 60; const timerEl = document.getElementById('timer')!; const interval = setInterval(() => { remaining--; timerEl.textContent = `⏱ ${formatTime(remaining)}`; if (remaining <= 0) { clearInterval(interval); alert('Time is up! Submitting your exam.'); (window as any).submitExam(); } }, 1000); (window as any)._examTimer = interval; } } function renderQuestionTake(q: any, index: number): string { const inputName = `q-${q.id}`; switch (q.type) { case 'multiple_choice': return `
${index + 1}. ${escapeHtml(q.text)} (${q.points} pts)
${q.options?.map((opt: string, i: number) => ` `).join('') ?? ''}
`; case 'true_false': return `
${index + 1}. ${escapeHtml(q.text)} (${q.points} pts)
`; case 'fill_blank': return `
${index + 1}. ${escapeHtml(q.text)} (${q.points} pts)
`; case 'short_answer': return `
${index + 1}. ${escapeHtml(q.text)} (${q.points} pts)
`; case 'essay': return `
${index + 1}. ${escapeHtml(q.text)} (${q.points} pts)
`; default: return ''; } } (window as any).submitExam = () => { const studentName = (document.getElementById('student-name') as HTMLInputElement)?.value.trim(); if (!studentName) { alert('Please enter your name'); return; } const exams = loadExams(); const exam = exams.find(e => e.id === takingExamId); if (!exam) return; const answers: any[] = []; exam.questions.forEach(q => { const inputName = `q-${q.id}`; if (q.type === 'multiple_choice' || q.type === 'true_false') { const selected = document.querySelector(`input[name="${inputName}"]:checked`) as HTMLInputElement; if (selected) answers.push({ questionId: q.id, value: selected.value }); } else { const el = document.querySelector(`[name="${inputName}"]`) as HTMLInputElement | HTMLTextAreaElement; if (el) answers.push({ questionId: q.id, value: el.value }); } }); if ((window as any)._examTimer) clearInterval((window as any)._examTimer); const submission: Submission = { id: generateId(), examId: exam.id, studentName, startedAt: new Date().toISOString(), completedAt: new Date().toISOString(), answers, scores: {}, totalScore: 0, maxScore: 0, percentage: 0, graded: false, }; const questionsMap = new Map(exam.questions.map(q => [q.id, q])); const graded = autoGradeSubmission(submission, questionsMap); const submissions = loadSubmissions(); submissions.push(graded); saveSubmissions(submissions); if (exam.showResults) { alert(`Submitted! You scored ${graded.percentage}% (${graded.totalScore}/${graded.maxScore})`); } else { alert('Submitted! Your teacher will review your answers.'); } navigate('dashboard'); }; function renderGrade() { const submissions = loadSubmissions(); const submission = submissions.find(s => s.id === gradingSubmissionId); const exams = loadExams(); const exam = submission ? exams.find(e => e.id === submission.examId) : null; if (!submission || !exam) { app.innerHTML = `

Submission not found.

`; return; } app.innerHTML = ` ${renderNav()}

Grade: ${escapeHtml(submission.studentName)}

${escapeHtml(exam.title)}

Current: ${submission.totalScore} / ${submission.maxScore} = ${submission.percentage}%
${exam.questions.map(q => { const answer = submission.answers.find(a => a.questionId === q.id); const score = submission.scores[q.id] ?? 0; const needsManual = q.type === 'short_answer' || q.type === 'essay'; const autoGraded = !needsManual && submission.scores[q.id] !== undefined; return `
${escapeHtml(q.text)} (${q.points} pts)
Answer: ${escapeHtml(String(answer?.value ?? 'No answer'))}
${q.correctAnswer ? `
Expected: ${escapeHtml(Array.isArray(q.correctAnswer) ? q.correctAnswer.join(' or ') : q.correctAnswer)}
` : ''}
${autoGraded ? 'Auto-graded' : (needsManual ? 'Manual' : '')}
`; }).join('')}
`; } (window as any).updateScore = (qid: string, maxPoints: number) => { const el = document.getElementById(`score-${qid}`) as HTMLInputElement; let val = parseFloat(el.value); if (val < 0) val = 0; if (val > maxPoints) val = maxPoints; el.value = String(val); }; (window as any).saveGrades = () => { const submissions = loadSubmissions(); const submission = submissions.find(s => s.id === gradingSubmissionId); if (!submission) return; const exams = loadExams(); const exam = exams.find(e => e.id === submission.examId); if (!exam) return; let totalScore = 0; const scores: Record = {}; exam.questions.forEach(q => { const el = document.getElementById(`score-${q.id}`) as HTMLInputElement; const score = parseFloat(el.value) || 0; scores[q.id] = score; totalScore += score; }); const maxScore = exam.questions.reduce((a, q) => a + q.points, 0); const percentage = maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0; const idx = submissions.findIndex(s => s.id === gradingSubmissionId); submissions[idx] = { ...submission, scores, totalScore, maxScore, percentage, graded: true, gradedAt: new Date().toISOString(), }; saveSubmissions(submissions); alert('Grades saved!'); navigate('dashboard'); }; function renderResults() { const exams = loadExams(); const exam = exams.find(e => e.id === viewingResultsExamId); const submissions = loadSubmissions().filter(s => s.examId === viewingResultsExamId); if (!exam) { app.innerHTML = `

Exam not found.

`; return; } const avgScore = submissions.length > 0 ? Math.round(submissions.reduce((a, s) => a + s.percentage, 0) / submissions.length) : 0; const gradedCount = submissions.filter(s => s.graded).length; app.innerHTML = ` ${renderNav()}

Results: ${escapeHtml(exam.title)}

${submissions.length}
Submissions
${gradedCount}
Graded
${avgScore}%
Average
${submissions.length > 0 ? Math.max(...submissions.map(s => s.percentage)) : 0}%
Highest

Submissions

${submissions.length === 0 ? '

No submissions yet.

' : `
${submissions.map(sub => `
${escapeHtml(sub.studentName)} ${sub.percentage}% ${sub.graded ? 'Graded' : 'Pending'} ${new Date(sub.completedAt).toLocaleDateString()}
`).join('')}
`}

Question Breakdown

${exam.questions.map(q => { const questionSubmissions = submissions.map(sub => { const answer = sub.answers.find(a => a.questionId === q.id); return { score: sub.scores[q.id] ?? 0, answer: answer?.value ?? '' }; }); const avgQScore = questionSubmissions.length > 0 ? Math.round((questionSubmissions.reduce((a, s) => a + s.score, 0) / (questionSubmissions.length * q.points)) * 100) : 0; return `
${escapeHtml(q.text.substring(0, 60))}${q.text.length > 60 ? '...' : ''}
${avgQScore}% avg
`; }).join('')}
`; } function renderSubmissions() { navigate('dashboard'); } function renderExport() { app.innerHTML = ` ${renderNav()}

Import / Export

Export Exams

Download all your exams as a JSON file.

Export Submissions

Download all submissions as a JSON file.

Import Exams

Upload a previously exported exam file.

`; } (window as any).exportExams = () => { exportToJSON(loadExams(), `intelligrade-exams-${new Date().toISOString().slice(0, 10)}.json`); }; (window as any).exportSubmissions = () => { exportToJSON(loadSubmissions(), `intelligrade-submissions-${new Date().toISOString().slice(0, 10)}.json`); }; (window as any).importExams = async () => { const input = document.getElementById('import-file') as HTMLInputElement; const file = input.files?.[0]; if (!file) return; try { const data = await importFromJSON(file) as Exam[]; if (!Array.isArray(data)) { alert('Invalid file format'); return; } const existing = loadExams(); const merged = [...existing]; for (const exam of data) { if (!existing.find(e => e.id === exam.id)) { exam.id = generateId(); // Reassign IDs to avoid conflicts exam.createdAt = new Date().toISOString(); merged.push(exam); } } saveExams(merged); alert(`Imported ${data.length} exams`); input.value = ''; navigate('dashboard'); } catch (e) { alert('Failed to import: ' + (e as Error).message); } }; // Initial render render();