Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1" /> | |
| <title>AI PDF & Video → MCQ Generator</title> | |
| <!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> --> | |
| <!-- Updated Bootstrap 5.3.2 --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <style> | |
| body { | |
| background: url("https://guptadeepak.com/content/images/size/w2000/2024/07/The-Future-of-AI-and-Its-Impact-on-Humanity.webp") no-repeat center center fixed; | |
| background-size: cover; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| } | |
| body::before { | |
| content: ""; | |
| position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(255,255,255,0.09); | |
| backdrop-filter: blur(8px); | |
| z-index: -1; | |
| } | |
| header { | |
| background: linear-gradient(to right, #0d47a1, #c62828); | |
| padding: 10px 20px; | |
| color: white; | |
| /* display: none; */ | |
| } | |
| /* Login Page */ | |
| /* #loginPage { | |
| height: 100vh; display: flex; justify-content: center; align-items: center; | |
| } | |
| .login-card { | |
| background: rgba(255, 255, 255, 0.15); | |
| border-radius: 16px; | |
| padding: 40px; | |
| width: 380px; | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.3); | |
| backdrop-filter: blur(10px); | |
| animation: fadeInUp 1s ease forwards; | |
| } | |
| .login-card h3 { | |
| text-align: center; | |
| margin-bottom: 25px; | |
| color: #0d47a1; | |
| font-weight: 700; | |
| } | |
| .form-control { border-radius: 8px; border: 1px solid #1976d2; } | |
| .btn-login { | |
| background: linear-gradient(135deg, #1976d2, #42a5f5); | |
| color: #fff; border: none; | |
| width: 100%; padding: 12px; border-radius: 10px; | |
| font-size: 1.1rem; margin-top: 15px; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); | |
| } | |
| .btn-login:hover { | |
| transform: translateY(-5px) scale(1.03); | |
| box-shadow: 0 0 15px rgba(255,255,255,0.6), | |
| 0 0 30px rgba(255,255,255,0.4), | |
| 0 8px 25px rgba(0,0,0,0.4); | |
| filter: brightness(1.1); | |
| } | |
| #loginError { display:none; text-align:center; margin-top:10px; } */ | |
| /* Stats card */ | |
| .stats-card { | |
| border-radius: 12px; padding: 20px; text-align: center; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| opacity: 0; transform: translateY(40px); | |
| animation: fadeInUp 1s ease forwards; transition: all 0.3s ease; | |
| background: linear-gradient(135deg, #1976d2, #42a5f5); color: #fff; | |
| } | |
| .stats-card:hover { | |
| transform: translateY(-10px) scale(1.05); | |
| box-shadow: 0 0 15px rgba(255,255,255,0.6), 0 0 30px rgba(255,255,255,0.4), 0 8px 25px rgba(0,0,0,0.4); | |
| filter: brightness(1.2); | |
| } | |
| .section-card { | |
| background: linear-gradient(135deg, #e3f2fd, #bbdefb); | |
| border-radius: 12px; padding: 25px; margin-bottom: 40px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| opacity: 0; transform: scale(0.9) translateY(40px); | |
| animation: fadePop 1s ease forwards; transition: all 0.3s ease; | |
| } | |
| .upload-box { | |
| border: 2px dashed #0d47a1; padding: 25px; border-radius: 12px; | |
| background: rgba(255,255,255,0.9); text-align: center; | |
| } | |
| footer { | |
| background: linear-gradient(to right, #c62828, #0d47a1); | |
| color: #fff; padding: 12px; text-align: center; font-size:0.9rem; | |
| /* display: none; */ | |
| } | |
| @keyframes fadeInUp { from {opacity:0; transform:translateY(40px);} to {opacity:1; transform:translateY(0);} } | |
| @keyframes fadePop { from {opacity:0; transform:scale(0.9) translateY(40px);} to {opacity:1; transform:scale(1) translateY(0);} } | |
| /* Main nav */ | |
| .main-nav { height:80vh; display:flex; justify-content:center; align-items:center; display:none; } | |
| .nav-vertical { display:flex; flex-direction:column; gap:20px; align-items:center; } | |
| .nav-vertical .btn { | |
| padding: 15px 30px; font-size: 1.1rem; border-radius: 12px; | |
| color: #fff; background: linear-gradient(135deg, #1976d2, #42a5f5); | |
| border: none; box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| opacity: 0; transform: translateY(40px); | |
| animation: fadeInUp 1s ease forwards; transition: all 0.3s ease; | |
| width: 400px; text-align: left; padding-left: 20px; | |
| } | |
| .nav-vertical .btn:hover { | |
| transform: translateY(-10px) scale(1.04); | |
| box-shadow: 0 0 15px rgba(255,255,255,0.6), 0 0 30px rgba(255,255,255,0.4), 0 8px 25px rgba(0,0,0,0.4); | |
| filter: brightness(1.12); | |
| } | |
| .page { display: none; padding-top: 18px; } | |
| /* Questions table styles */ | |
| .question-table { | |
| max-height: 600px; | |
| overflow-y: auto; | |
| } | |
| .question-row { | |
| border-left: 4px solid #1976d2; | |
| } | |
| .question-row.mcq { | |
| border-left-color: #28a745; | |
| } | |
| .question-row.descriptive { | |
| border-left-color: #ffc107; | |
| } | |
| .ai-card { | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 20px; | |
| padding: 40px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 0 30px rgba(0, 255, 255, 0.6), | |
| 0 0 60px rgba(0, 119, 255, 0.4); | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .ai-card:hover { | |
| box-shadow: 0 0 50px rgba(0, 255, 255, 0.9), | |
| 0 0 100px rgba(0, 119, 255, 0.6); | |
| transform: scale(1.05); | |
| } | |
| .brand-title { | |
| font-weight: bold; | |
| font-size: 50px; | |
| color: #fff; | |
| text-shadow: 0 0 20px cyan, 0 0 40px #007bff, 0 0 60px #00e5ff; | |
| letter-spacing: 2px; | |
| } | |
| /* Sticky table header */ | |
| .table thead th { | |
| position: sticky; | |
| top: 0; | |
| background: #f8f9fa; /* Bootstrap table-light background */ | |
| z-index: 2; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Login --> | |
| <!-- <div id="loginPage"> | |
| <div class="login-card"> | |
| <h3>🔐 Login</h3> | |
| <div class="mb-3"><label class="form-label">Username</label> | |
| <input type="text" id="username" class="form-control" placeholder="Enter username"></div> | |
| <div class="mb-3"><label class="form-label">Password</label> | |
| <input type="password" id="password" class="form-control" placeholder="Enter password"></div> | |
| <button class="btn-login" onclick="login()">Login</button> | |
| <div id="loginError" class="text-danger small">⚠ Invalid username or password</div> | |
| </div> | |
| </div> --> | |
| <!-- Header --> | |
| <header class="d-flex justify-content-between align-items-center" id="mainHeader"> | |
| <img src="http://icfaionline.in/assets/img/icfai-logo.png" class="p-1" alt="ICFAI Logo" height="60" /> | |
| <!-- <button class="btn btn-light btn-sm" onclick="logout()">Logout</button> --> | |
| </header> | |
| <!-- NAV --> | |
| <div id="mainPage" class="main-nav"> | |
| <div class="container"> | |
| <div class="row"> | |
| <div class="col-xl-10 d-flex align-items-center justify-content-between"> | |
| <!-- ICFAI AI Solutions Text --> | |
| <div class="brand-title" style="padding:15px; font-weight:bold; font-size:50px; color:#fff;"> | |
| <div class="ai-card"> | |
| <div class="brand-title"> | |
| ICFAI AI Solutions | |
| </div> | |
| </div> | |
| </div> | |
| <!-- NAV --> | |
| <div class="nav-vertical"> | |
| <button class="btn" data-page="pdfPage">Generate Questions from PDFs</button> | |
| <button class="btn" data-page="videoPage">Generate Questions from Video</button> | |
| <button class="btn" data-page="questionsPage">View Generated Questions</button> | |
| <button class="btn" data-page="generatePaperPage">Generate Question Paper</button> | |
| <a class="btn" href="http://192.168.1.77:8501" target="_blank">Multilingual Video Generator</a> | |
| <a class="btn" href="http://192.168.1.56/Summarizer/" target="_blank">Text Summarizer</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- PDF PAGE --> | |
| <div id="pdfPage" class="page container my-5"> | |
| <h3 class="step-title"> Generate Questions from PDFs </h3> | |
| <div class="row g-3 mb-4"> | |
| <button class="btn btn-light btn-sm float-end" style="margin-left:10px" onclick="goHome()">🏠 Home</button> | |
| <div class="col-md-3"><div class="stats-card"><h6>PDFs uploaded</h6><h3>0</h3></div></div> | |
| <div class="col-md-3"><div class="stats-card"><h6>Pages (current PDF)</h6><h3>0</h3></div></div> | |
| <div class="col-md-3"><div class="stats-card"><h6>MCQs generated</h6><h3>0</h3></div></div> | |
| <div class="col-md-3"><div class="stats-card"><h6>Descriptive Qs</h6><h3>0</h3></div></div> | |
| </div> | |
| <div class="section-card"> | |
| <div class="upload-box"> | |
| <img src="https://img.icons8.com/ios-filled/70/0d47a1/pdf.png" alt="PDF Upload" class="mb-2"/> | |
| <p>Drag and drop file here</p> | |
| <p><small>Limit 200MB per file • PDF</small></p> | |
| <input id="pdfUploader" type="file" class="form-control form-control-sm btn-upload" accept=".pdf"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- VIDEO PAGE --> | |
| <div id="videoPage" class="page container my-5"> | |
| <h3 class="step-title"> Generate Questions from Video </h3> | |
| <button class="btn btn-light btn-sm float-end" style="margin-left:10px" onclick="goHome()">🏠 Home</button> | |
| <div class="row g-3 mb-4"> | |
| <div class="col-md-3"><div class="stats-card"><h6>Videos uploaded</h6><h3 id="videoUploadsStat">0</h3></div></div> | |
| <div class="col-md-3"><div class="stats-card"><h6>MCQs generated</h6><h3 id="videoMcqStat">0</h3></div></div> | |
| </div> | |
| <div class="section-card"> | |
| <div class="upload-box"> | |
| <img src="https://img.icons8.com/ios-filled/70/c62828/video.png" alt="Video Upload" class="mb-2"/> | |
| <p>Drag and drop file here</p> | |
| <p><small>Limit 200MB per file • MP4, MOV, MKV, AVI, MPEG4</small></p> | |
| <input id="videoUploader" type="file" class="form-control form-control-sm btn-upload" accept="video/*"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- QUESTIONS PAGE --> | |
| <div id="questionsPage" class="page container my-5"> | |
| <button class="btn btn-light btn-sm float-end" style="margin-left:10px" onclick="goHome()">🏠 Home</button> | |
| <h3 class="step-title"> Generated Questions in Database </h3> | |
| <div class="section-card"> | |
| <!-- Remove the form element and keep just the search controls --> | |
| <div class="row mb-3"> | |
| <div class="col-md-6"> | |
| <input type="text" id="searchInput" class="form-control" placeholder="Search by topic or keyword..."> | |
| </div> | |
| <div class="col-md-4"> | |
| <select id="questionTypeSelect" class="form-select"> | |
| <option value="all">All Types</option> | |
| <option value="MCQ">MCQ Only</option> | |
| <option value="Descriptive">Descriptive Only</option> | |
| </select> | |
| </div> | |
| <div class="col-md-2"> | |
| <!-- Change to regular button, not submit --> | |
| <button type="button" id="searchBtn" class="btn btn-primary w-100">Search</button> | |
| </div> | |
| </div> | |
| <div id="questionsCount" class="mb-3"></div> | |
| <div id="questionsTable" class="question-table"></div> | |
| </div> | |
| </div> | |
| <!-- Generate Question Paper Page --> | |
| <div id="generatePaperPage" class="page container my-5"> | |
| <button class="btn btn-light btn-sm float-end" style="margin-left:10px" onclick="goHome()">🏠 Home</button> | |
| <h3 class="step-title">Generate Question Paper</h3> | |
| <div class="section-card"> | |
| <div class="row mb-4"> | |
| <div class="col-md-6"> | |
| <h5>Question Distribution by Difficulty Level</h5> | |
| <div class="filter-section"> | |
| <div class="row g-3"> | |
| <div class="col-md-4"> | |
| <label class="form-label">Level 1 </label> | |
| <input type="number" id="level1Count" class="form-control level-input" min="0" value="0"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Level 2</label> | |
| <input type="number" id="level2Count" class="form-control level-input" min="0" value="0"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Level 3</label> | |
| <input type="number" id="level3Count" class="form-control level-input" min="0" value="0"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Level 4</label> | |
| <input type="number" id="level4Count" class="form-control level-input" min="0" value="0"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Level 5</label> | |
| <input type="number" id="level5Count" class="form-control level-input" min="0" value="0"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <h5>Question Types & Topics</h5> | |
| <div class="filter-section"> | |
| <div class="mb-3"> | |
| <label class="form-label">Question Types</label> | |
| <div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="typeMCQ" value="mcq" checked> | |
| <label class="form-check-label" for="typeMCQ">MCQ</label> | |
| </div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="checkbox" id="typeDescriptive" value="descriptive" checked> | |
| <label class="form-check-label" for="typeDescriptive">Descriptive</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Topic Selection</label> | |
| <div> | |
| <div class="form-check"> | |
| <input class="form-check-input" type="radio" name="chapterScope" id="allChapters" value="all" checked> | |
| <label class="form-check-label" for="allChapters"> | |
| All Topics | |
| </label> | |
| </div> | |
| <div class="form-check"> | |
| <input class="form-check-input" type="radio" name="chapterScope" id="selectedChapters" value="selected"> | |
| <label class="form-check-label" for="selectedChapters"> | |
| Selected Topics Only | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-3" id="chaptersSelectionContainer" style="display: none;"> | |
| <label class="form-label">Select Topics</label> | |
| <select id="chaptersSelect" class="form-select topic-select" multiple> | |
| <!-- Chapters will be loaded dynamically --> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-12"> | |
| <button class="btn btn-primary btn-lg" onclick="generateQuestionPaper()"> | |
| 🎯 Generate Question Paper | |
| </button> | |
| <div id="paperGenerationStatus" class="mt-3"></div> | |
| </div> | |
| </div> | |
| <div class="row mt-4"> | |
| <div class="col-12"> | |
| <div id="generatedPaperResults" class="mt-2 p-2" style="background:#ffffff; border-radius:8px; border:1px solid #e9ecef; max-height:60vh; overflow:auto;"> | |
| <div class="text-center text-muted"> | |
| Configure your question paper above and click "Generate Question Paper" | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ADD THE QUESTION PAPER SECTION HERE --> | |
| <!-- Generate Question Paper (REPLACEMENT) --> | |
| <style> | |
| /* Filter section styles */ | |
| .filter-section { | |
| background: rgba(255, 255, 255, 0.9); | |
| border-radius: 10px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| .level-input { | |
| width: 70px; | |
| text-align: center; | |
| } | |
| .topic-select { | |
| min-height: 120px; | |
| } | |
| /* local styles for editable questions table inside resultsDiv */ | |
| #paperControls { margin-top: 10px; } | |
| #paperResultsTable { width: 100%; table-layout: fixed; word-break: break-word; } | |
| #paperResultsTable th, #paperResultsTable td { vertical-align: top; white-space: normal; } | |
| .editable { min-height: 28px; padding: 6px; border-radius: 4px; } | |
| .small-action { font-size: 0.9rem; } | |
| .select-col { width: 40px; text-align: center; } | |
| .id-col { width: 60px; } | |
| .type-col { width: 80px; } | |
| .actions-col { width: 110px; } | |
| </style> | |
| <script> | |
| // Helper state for paper UI (client-only) | |
| const PAPER_STATE = { | |
| loadedQuestions: [], // {id, topic, type, question, option_a..d, difficulty, flagged} | |
| selectedIds: new Set(), | |
| availableTopics: [] | |
| }; | |
| // Add this helper function for notifications if not already present | |
| function showNotification(message, type = 'info') { | |
| // Create a simple notification | |
| const notification = document.createElement('div'); | |
| notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`; | |
| notification.style.position = 'fixed'; | |
| notification.style.top = '20px'; | |
| notification.style.right = '20px'; | |
| notification.style.zIndex = '9999'; | |
| notification.style.minWidth = '300px'; | |
| notification.innerHTML = ` | |
| ${message} | |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | |
| `; | |
| document.body.appendChild(notification); | |
| // Auto remove after 3 seconds | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 3000); | |
| } | |
| // Load saved questions from /questions endpoint and render editable table | |
| async function loadSavedQuestionsForPaper() { | |
| const resultsDiv = document.getElementById('resultsDiv'); | |
| resultsDiv.innerHTML = '<div class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> Loading saved questions...</div>'; | |
| try { | |
| // Remove the ?flagged=true parameter to load ALL questions | |
| const resp = await fetch(`${backendBase}/questions`); | |
| const json = await resp.json(); | |
| // Keep only relevant fields and normalize type | |
| PAPER_STATE.loadedQuestions = (json || []).map(q => ({ | |
| id: q.id, | |
| topic: q.topic || '', | |
| type: (q.type || '').toUpperCase(), | |
| question: q.question || '', | |
| option_a: q.option_a || '', | |
| option_b: q.option_b || '', | |
| option_c: q.option_c || '', | |
| option_d: q.option_d || '', | |
| difficulty: (q.difficulty && q.difficulty.toString()) || '', | |
| flagged: q.flagged !== undefined ? q.flagged : true // Handle null/undefined as true | |
| })); | |
| // default select none | |
| PAPER_STATE.selectedIds = new Set(); | |
| renderEditableQuestionsTable(); | |
| } catch (err) { | |
| console.error('Error loading saved questions for paper:', err); | |
| resultsDiv.innerHTML = '<div class="alert alert-danger">Error loading saved questions.</div>'; | |
| } | |
| updatePaperSummary(); | |
| } | |
| // Render editable table into resultsDiv | |
| function renderEditableQuestionsTable() { | |
| const resultsDiv = document.getElementById('resultsDiv'); | |
| if (!PAPER_STATE.loadedQuestions.length) { | |
| resultsDiv.innerHTML = '<div class="alert alert-info small mb-0">No flagged questions found. Questions removed from previous sessions will not appear here.</div>'; | |
| return; | |
| } | |
| const table = document.createElement('table'); | |
| table.id = 'paperResultsTable'; | |
| table.className = 'table table-sm table-bordered'; | |
| const thead = document.createElement('thead'); | |
| thead.innerHTML = ` | |
| <tr> | |
| <th class="select-col"><input id="paperSelectAllToggle" type="checkbox" title="Select all"></th> | |
| <th class="id-col">ID</th> | |
| <th class="type-col">Type</th> | |
| <th>Flagged Status</th> | |
| <th>Topic</th> | |
| <th>Question (click to edit)</th> | |
| <th>Options (click to edit; MCQ only)</th> | |
| <th style="width:110px">Difficulty</th> | |
| <th class="actions-col">Actions</th> | |
| </tr> | |
| `; | |
| table.appendChild(thead); | |
| const tbody = document.createElement('tbody'); | |
| PAPER_STATE.loadedQuestions.forEach(q => { | |
| const tr = document.createElement('tr'); | |
| tr.dataset.qid = q.id; | |
| // Checkbox | |
| const chkTd = document.createElement('td'); | |
| chkTd.className = 'select-col text-center align-middle'; | |
| const chk = document.createElement('input'); | |
| chk.type = 'checkbox'; | |
| chk.className = 'paper-select-row'; | |
| chk.checked = PAPER_STATE.selectedIds.has(q.id); | |
| chk.addEventListener('change', (ev) => { | |
| if (ev.target.checked) PAPER_STATE.selectedIds.add(q.id); | |
| else PAPER_STATE.selectedIds.delete(q.id); | |
| updatePaperSummary(); | |
| }); | |
| chkTd.appendChild(chk); | |
| tr.appendChild(chkTd); | |
| // ID | |
| const idTd = document.createElement('td'); | |
| idTd.textContent = q.id; | |
| tr.appendChild(idTd); | |
| // Type | |
| const typeTd = document.createElement('td'); | |
| typeTd.innerHTML = `<span class="badge ${q.type === 'MCQ' ? 'bg-success' : 'bg-warning'}">${q.type}</span>`; | |
| tr.appendChild(typeTd); | |
| // Flagged Status | |
| const flagTd = document.createElement('td'); | |
| const flagStatus = q.flagged === true ? | |
| '<span class="badge bg-success">Approved</span>' : | |
| q.flagged === false ? | |
| '<span class="badge bg-danger">Not Approved</span>' : | |
| '<span class="badge bg-secondary">Pending</span>'; | |
| flagTd.innerHTML = flagStatus; | |
| tr.appendChild(flagTd); | |
| // Topic | |
| const topicTd = document.createElement('td'); | |
| const topicDiv = document.createElement('div'); | |
| topicDiv.className = 'editable'; | |
| topicDiv.contentEditable = 'true'; | |
| topicDiv.innerText = q.topic || ''; | |
| topicDiv.addEventListener('input', (ev) => { | |
| q.topic = topicDiv.innerText.trim(); | |
| }); | |
| topicTd.appendChild(topicDiv); | |
| tr.appendChild(topicTd); | |
| // Question (editable) | |
| const qTd = document.createElement('td'); | |
| const qDiv = document.createElement('div'); | |
| qDiv.className = 'editable'; | |
| qDiv.contentEditable = 'true'; | |
| qDiv.innerText = q.question; | |
| qDiv.addEventListener('input', (ev) => { | |
| q.question = qDiv.innerText.trim(); | |
| }); | |
| qTd.appendChild(qDiv); | |
| tr.appendChild(qTd); | |
| // Options | |
| const optsTd = document.createElement('td'); | |
| if (q.type === 'MCQ') { | |
| const optsWrapper = document.createElement('div'); | |
| optsWrapper.style.display = 'grid'; | |
| optsWrapper.style.gridTemplateColumns = '1fr 1fr'; | |
| optsWrapper.style.gap = '6px'; | |
| ['option_a','option_b','option_c','option_d'].forEach((optKey, i) => { | |
| const lbl = ['A','B','C','D'][i]; | |
| const optBox = document.createElement('div'); | |
| const optLabel = document.createElement('div'); | |
| optLabel.style.fontWeight = '600'; | |
| optLabel.style.fontSize = '0.85rem'; | |
| optLabel.textContent = lbl + ')'; | |
| const optDiv = document.createElement('div'); | |
| optDiv.className = 'editable'; | |
| optDiv.contentEditable = 'true'; | |
| optDiv.innerText = q[optKey] || ''; | |
| optDiv.addEventListener('input', () => { | |
| q[optKey] = optDiv.innerText.trim(); | |
| }); | |
| optBox.appendChild(optLabel); | |
| optBox.appendChild(optDiv); | |
| optsWrapper.appendChild(optBox); | |
| }); | |
| optsTd.appendChild(optsWrapper); | |
| } else { | |
| optsTd.innerHTML = `<div class="text-muted small">N/A for descriptive</div>`; | |
| } | |
| tr.appendChild(optsTd); | |
| // Difficulty (editable select) | |
| const diffTd = document.createElement('td'); | |
| const sel = document.createElement('select'); | |
| sel.className = 'form-select form-select-sm'; | |
| const blankOpt = document.createElement('option'); blankOpt.value = ''; blankOpt.textContent = '—'; | |
| sel.appendChild(blankOpt); | |
| for (let i = 1; i <= 5; i++) { | |
| const o = document.createElement('option'); o.value = String(i); o.textContent = String(i); | |
| if (String(q.difficulty) === String(i)) o.selected = true; | |
| sel.appendChild(o); | |
| } | |
| sel.addEventListener('change', () => { q.difficulty = sel.value; }); | |
| diffTd.appendChild(sel); | |
| tr.appendChild(diffTd); | |
| // Actions | |
| const actTd = document.createElement('td'); | |
| actTd.className = 'text-center'; | |
| // Approve button | |
| const approveBtn = document.createElement('button'); | |
| approveBtn.className = 'btn btn-sm btn-success me-1'; | |
| approveBtn.innerHTML = '✓ Approve'; | |
| approveBtn.addEventListener('click', async () => { | |
| const success = await updateQuestionFlag(q.id, true); | |
| if (success) { | |
| q.flagged = true; | |
| renderEditableQuestionsTable(); | |
| } | |
| }); | |
| // Reject button | |
| const rejectBtn = document.createElement('button'); | |
| rejectBtn.className = 'btn btn-sm btn-danger'; | |
| rejectBtn.innerHTML = '✗ Reject'; | |
| rejectBtn.addEventListener('click', async () => { | |
| const success = await updateQuestionFlag(q.id, false); | |
| if (success) { | |
| q.flagged = false; | |
| renderEditableQuestionsTable(); | |
| } | |
| }); | |
| actTd.appendChild(approveBtn); | |
| actTd.appendChild(rejectBtn); | |
| tr.appendChild(actTd); | |
| tbody.appendChild(tr); | |
| }); | |
| table.appendChild(tbody); | |
| // replace content | |
| resultsDiv.innerHTML = ''; | |
| resultsDiv.appendChild(table); | |
| // wire select all checkbox | |
| const paperSelectAllToggle = document.getElementById('paperSelectAllToggle'); | |
| if (paperSelectAllToggle) { | |
| paperSelectAllToggle.checked = PAPER_STATE.selectedIds.size === PAPER_STATE.loadedQuestions.length && PAPER_STATE.loadedQuestions.length > 0; | |
| paperSelectAllToggle.addEventListener('change', (ev) => { | |
| if (ev.target.checked) { | |
| PAPER_STATE.loadedQuestions.forEach(q => PAPER_STATE.selectedIds.add(q.id)); | |
| } else { | |
| PAPER_STATE.selectedIds.clear(); | |
| } | |
| // update all checkboxes in table | |
| table.querySelectorAll('.paper-select-row').forEach((c, i) => c.checked = ev.target.checked); | |
| updatePaperSummary(); | |
| }); | |
| } | |
| updatePaperSummary(); | |
| } | |
| // Summary text | |
| function updatePaperSummary() { | |
| const summaryEl = document.getElementById('paperSelectionSummary'); | |
| const totalLoaded = PAPER_STATE.loadedQuestions.length; | |
| const selected = PAPER_STATE.selectedIds.size; | |
| summaryEl.textContent = `${selected} selected • ${totalLoaded} flagged questions loaded`; | |
| } | |
| // Initialize event listeners once (not inside render function) | |
| function initializePaperEventListeners() { | |
| // Select all button | |
| document.getElementById('selectAllPaperBtn').addEventListener('click', () => { | |
| PAPER_STATE.loadedQuestions.forEach(q => PAPER_STATE.selectedIds.add(q.id)); | |
| // refresh table checkboxes | |
| document.querySelectorAll('#paperResultsTable .paper-select-row').forEach(c => c.checked = true); | |
| updatePaperSummary(); | |
| }); | |
| // Clear selection button | |
| document.getElementById('clearSelectionPaperBtn').addEventListener('click', () => { | |
| PAPER_STATE.selectedIds.clear(); | |
| document.querySelectorAll('#paperResultsTable .paper-select-row').forEach(c => c.checked = false); | |
| updatePaperSummary(); | |
| }); | |
| // Load button | |
| document.getElementById('loadSavedForPaperBtn').addEventListener('click', async () => { | |
| await loadSavedQuestionsForPaper(); | |
| }); | |
| // Approve Selected button | |
| document.getElementById('approveSelectedBtn').addEventListener('click', async () => { | |
| await bulkUpdateFlags(true); | |
| }); | |
| // Reject Selected button | |
| document.getElementById('rejectSelectedBtn').addEventListener('click', async () => { | |
| await bulkUpdateFlags(false); | |
| }); | |
| } | |
| // Bulk update function for approve/reject | |
| function initializeQuestionTableEventListeners() { | |
| // Editable field event listeners | |
| document.querySelectorAll('#questionsTable .editable').forEach(element => { | |
| element.addEventListener('blur', function() { | |
| const questionId = this.getAttribute('data-id'); | |
| const field = this.getAttribute('data-field'); | |
| const value = this.innerText.trim(); | |
| if (questionId && field) { | |
| updateQuestionField(questionId, field, value); | |
| } | |
| }); | |
| // Add Enter key support to save on Enter | |
| element.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| this.blur(); | |
| } | |
| }); | |
| }); | |
| // Difficulty select event listeners | |
| document.querySelectorAll('#questionsTable .difficulty-select').forEach(select => { | |
| select.addEventListener('change', function() { | |
| const questionId = this.getAttribute('data-id'); | |
| const value = this.value; | |
| if (questionId) { | |
| updateQuestionField(questionId, 'difficulty', value); | |
| } | |
| }); | |
| }); | |
| // Approve button event listeners | |
| document.querySelectorAll('#questionsTable .approve-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const questionId = this.getAttribute('data-id'); | |
| if (questionId) { | |
| updateQuestionFlag(questionId, true); | |
| } | |
| }); | |
| }); | |
| // Reject button event listeners | |
| document.querySelectorAll('#questionsTable .reject-btn').forEach(button => { | |
| button.addEventListener('click', function() { | |
| const questionId = this.getAttribute('data-id'); | |
| if (questionId) { | |
| updateQuestionFlag(questionId, false); | |
| } | |
| }); | |
| }); | |
| } | |
| // Bulk update function for approve/reject | |
| async function bulkUpdateFlags(flagged) { | |
| const selectedIds = Array.from(PAPER_STATE.selectedIds); | |
| if (selectedIds.length === 0) { | |
| alert('No questions selected.'); | |
| return; | |
| } | |
| const questionUpdates = selectedIds.map(id => ({ | |
| id: parseInt(id), | |
| flagged: flagged | |
| })); | |
| try { | |
| const response = await fetch(`${backendBase}/bulk_update_flags`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ question_updates: questionUpdates }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| // Update local state | |
| PAPER_STATE.loadedQuestions.forEach(q => { | |
| if (PAPER_STATE.selectedIds.has(q.id)) { | |
| q.flagged = flagged; | |
| } | |
| }); | |
| renderEditableQuestionsTable(); | |
| showNotification(`Successfully ${flagged ? 'approved' : 'Not Approved'} ${selectedIds.length} questions.`, 'success'); | |
| } else { | |
| showNotification(`Error updating questions: ${data.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error updating bulk flags:', error); | |
| showNotification('Error updating questions.', 'error'); | |
| } | |
| } | |
| // Function to clean option text by removing "Answer: X Difficulty: Y" patterns | |
| function cleanOptionText(optionText) { | |
| // Remove patterns like "Answer: B Difficulty: 4" from the end of the option text | |
| return optionText | |
| .replace(/\s*Answer:\s*[A-D]\s*Difficulty:\s*\d+$/, '') // Remove "Answer: X Difficulty: Y" | |
| .replace(/\s*Difficulty:\s*\d+\s*Answer:\s*[A-D]$/, '') // Remove "Difficulty: Y Answer: X" | |
| .replace(/\s*Answer:\s*[A-D]$/, '') // Remove just "Answer: X" | |
| .replace(/\s*Difficulty:\s*\d+$/, '') // Remove just "Difficulty: Y" | |
| .trim(); | |
| } | |
| // Function to download question paper as text file | |
| function downloadQuestionPaper(questions) { | |
| if (!Array.isArray(questions)) questions = []; | |
| const mcqs = questions.filter(q => (q.type || '').toUpperCase() === 'MCQ'); | |
| const descs = questions.filter(q => (q.type || '').toUpperCase() !== 'MCQ'); | |
| const ordered = [...mcqs, ...descs]; | |
| // Create Word document content with proper XML structure | |
| let wordContent = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> | |
| <?mso-application progid="Word.Document"?> | |
| <w:wordDocument | |
| xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" | |
| xmlns:v="urn:schemas-microsoft-com:vml" | |
| xmlns:w10="urn:schemas-microsoft-com:office:word" | |
| xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" | |
| xmlns:aml="http://schemas.microsoft.com/aml/2001/core" | |
| xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" | |
| xmlns:o="urn:schemas-microsoft-com:office:office" | |
| xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"> | |
| <w:body> | |
| <wx:sect> | |
| <w:p> | |
| <w:r> | |
| <w:rPr> | |
| <w:b/> | |
| <w:sz w:val="32"/> | |
| <w:sz-cs w:val="32"/> | |
| </w:rPr> | |
| <w:t>QUESTION PAPER</w:t> | |
| </w:r> | |
| </w:p> | |
| <w:p> | |
| <w:r> | |
| <w:t>${'='.repeat(50)}</w:t> | |
| </w:r> | |
| </w:p> | |
| <w:p><w:r><w:t></w:t></w:r></w:p>`; | |
| ordered.forEach((q, index) => { | |
| const cleanQuestion = cleanOptionText(q.question || ''); | |
| // Question number and text | |
| wordContent += ` | |
| <w:p> | |
| <w:r> | |
| <w:rPr> | |
| <w:b/> | |
| </w:rPr> | |
| <w:t>${index + 1}. ${cleanQuestion}</w:t> | |
| </w:r> | |
| </w:p>`; | |
| if ((q.type || '').toUpperCase() === 'MCQ') { | |
| // MCQ Options | |
| wordContent += ` | |
| <w:p> | |
| <w:r> | |
| <w:t> A) ${cleanOptionText(q.option_a || '')}</w:t> | |
| </w:r> | |
| </w:p> | |
| <w:p> | |
| <w:r> | |
| <w:t> B) ${cleanOptionText(q.option_b || '')}</w:t> | |
| </w:r> | |
| </w:p> | |
| <w:p> | |
| <w:r> | |
| <w:t> C) ${cleanOptionText(q.option_c || '')}</w:t> | |
| </w:r> | |
| </w:p> | |
| <w:p> | |
| <w:r> | |
| <w:t> D) ${cleanOptionText(q.option_d || '')}</w:t> | |
| </w:r> | |
| </w:p>`; | |
| } else { | |
| // Descriptive question indicator | |
| wordContent += ` | |
| <w:p> | |
| <w:r> | |
| <w:t> [Descriptive Question]</w:t> | |
| </w:r> | |
| </w:p>`; | |
| } | |
| // Add space between questions | |
| wordContent += `<w:p><w:r><w:t></w:t></w:r></w:p>`; | |
| }); | |
| // Close the document | |
| wordContent += ` | |
| </wx:sect> | |
| </w:body> | |
| </w:wordDocument>`; | |
| const blob = new Blob([wordContent], { | |
| type: 'application/msword' | |
| }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'question_paper.doc'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| } | |
| // On load: keep resultsDiv empty and summary set | |
| (function initPaperUI() { | |
| updatePaperSummary(); | |
| initializePaperEventListeners(); | |
| loadChaptersForPaper(); | |
| })(); | |
| // Add search functionality | |
| function initializeSearchFunctionality() { | |
| const searchForm = document.getElementById('searchForm'); | |
| const searchInput = document.getElementById('searchInput'); | |
| const questionTypeSelect = document.getElementById('questionTypeSelect'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| // Handle search button click | |
| if (searchBtn) { | |
| searchBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); // Prevent form submission | |
| e.stopPropagation(); // Stop event bubbling | |
| const searchTerm = searchInput ? searchInput.value : ''; | |
| const questionType = questionTypeSelect ? questionTypeSelect.value : 'all'; | |
| console.log('Searching for:', searchTerm, 'Type:', questionType); | |
| loadQuestionsFromDB(searchTerm, questionType); | |
| }); | |
| } | |
| // Handle form submission (as backup) | |
| if (searchForm) { | |
| searchForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); // This is crucial - prevents page refresh | |
| const searchTerm = searchInput ? searchInput.value : ''; | |
| const questionType = questionTypeSelect ? questionTypeSelect.value : 'all'; | |
| console.log('Form submit - Searching for:', searchTerm, 'Type:', questionType); | |
| loadQuestionsFromDB(searchTerm, questionType); | |
| }); | |
| } | |
| // Also trigger search when type changes | |
| if (questionTypeSelect) { | |
| questionTypeSelect.addEventListener('change', function() { | |
| const searchTerm = searchInput ? searchInput.value : ''; | |
| const questionType = this.value; | |
| console.log('Type changed - Searching for:', searchTerm, 'Type:', questionType); | |
| loadQuestionsFromDB(searchTerm, questionType); | |
| }); | |
| } | |
| } | |
| // Load topics for the question paper generation | |
| let LAST_CHAPTERS = []; // cached chapters from server | |
| // Load chapters (same UI as PDF page) | |
| async function loadChaptersForPaper() { | |
| try { | |
| const response = await fetch(`${backendBase}/questions`); | |
| const questions = await response.json(); | |
| // Extract unique chapters/topics | |
| const chapters = [...new Set(questions.map(q => q.topic).filter(topic => topic && topic.trim() !== ''))].sort(); | |
| const chaptersSelect = document.getElementById('chaptersSelect'); | |
| chaptersSelect.innerHTML = ''; | |
| chapters.forEach(chapter => { | |
| const option = document.createElement('option'); | |
| option.value = chapter; | |
| option.textContent = chapter; | |
| chaptersSelect.appendChild(option); | |
| }); | |
| console.log(`Loaded ${chapters.length} chapters for paper generation`); | |
| } catch (error) { | |
| console.error('Error loading chapters:', error); | |
| } | |
| } | |
| function getTopicsForSelectedChapters() { | |
| const checked = Array.from(document.querySelectorAll('.paper-chap-chk:checked')).map(i => i.value); | |
| if (!checked.length) return []; // empty means "all" | |
| // map checked chapter numbers to topics using cached LAST_CHAPTERS | |
| const topics = []; | |
| checked.forEach(chid => { | |
| const found = LAST_CHAPTERS.find(c => String(c.chapter) === String(chid)); | |
| if (found && Array.isArray(found.topics)) { | |
| found.topics.forEach(t => { | |
| if (t && !topics.includes(t)) topics.push(t); | |
| }); | |
| } | |
| }); | |
| return topics; | |
| } | |
| // Generate question paper | |
| async function generateQuestionPaper() { | |
| const statusEl = document.getElementById('paperGenerationStatus'); | |
| const resultsDiv = document.getElementById('generatedPaperResults'); | |
| statusEl.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Generating question paper...'; | |
| resultsDiv.innerHTML = ''; | |
| try { | |
| // Get level counts | |
| const levels = { | |
| 1: parseInt(document.getElementById('level1Count').value) || 0, | |
| 2: parseInt(document.getElementById('level2Count').value) || 0, | |
| 3: parseInt(document.getElementById('level3Count').value) || 0, | |
| 4: parseInt(document.getElementById('level4Count').value) || 0, | |
| 5: parseInt(document.getElementById('level5Count').value) || 0 | |
| }; | |
| // Get question types | |
| const types = { | |
| mcq: document.getElementById('typeMCQ').checked, | |
| descriptive: document.getElementById('typeDescriptive').checked | |
| }; | |
| // Get chapter selection | |
| const chapterScope = document.querySelector('input[name="chapterScope"]:checked').value; | |
| let selectedChapters = []; | |
| if (chapterScope === 'selected') { | |
| const chaptersSelect = document.getElementById('chaptersSelect'); | |
| selectedChapters = Array.from(chaptersSelect.selectedOptions).map(option => option.value); | |
| if (selectedChapters.length === 0) { | |
| statusEl.innerHTML = '<div class="alert alert-warning">Please select at least one chapter.</div>'; | |
| return; | |
| } | |
| } | |
| const requestData = { | |
| levels: levels, | |
| types: types, | |
| topics: chapterScope === 'all' ? 'all' : selectedChapters | |
| }; | |
| console.log('Generating paper with data:', requestData); | |
| const response = await fetch(`${backendBase}/generate_question_paper`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(requestData) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| // Reorder: MCQs first, then descriptive | |
| const allQuestions = Array.isArray(data.questions) ? data.questions : []; | |
| const mcqList = allQuestions.filter(q => (q.type || '').toUpperCase() === 'MCQ'); | |
| const descList = allQuestions.filter(q => (q.type || '').toUpperCase() !== 'MCQ'); | |
| const ordered = [...mcqList, ...descList]; | |
| statusEl.innerHTML = `<div class="alert alert-success">✅ Generated paper with ${data.total_selected} questions</div>`; | |
| // Add chapter scope info | |
| const scopeInfo = chapterScope === 'all' | |
| ? 'All Topics' | |
| : `Selected Topics: ${selectedChapters.join(', ')}`; | |
| statusEl.innerHTML += `<div class="mt-2 text-dark fw-semibold">Scope: ${scopeInfo}</div>`; | |
| // Display ordered paper (MCQs then descriptives) | |
| displayGeneratedPaper(ordered); | |
| // Show level summary | |
| const levelSummary = Object.entries(data.level_summary || {}) | |
| .map(([level, count]) => `Level ${level}: ${count}`) | |
| .join(', '); | |
| statusEl.innerHTML += `<div class="mt-2 text-dark fw-semibold">Distribution: ${levelSummary}</div>`; | |
| } else { | |
| statusEl.innerHTML = `<div class="alert alert-danger">❌ Error: ${data.error}</div>`; | |
| } | |
| } catch (error) { | |
| console.error('Error generating question paper:', error); | |
| statusEl.innerHTML = `<div class="alert alert-danger">❌ Error generating question paper: ${error.message}</div>`; | |
| } | |
| } | |
| // Initialize chapter selection functionality | |
| function initializeChapterSelection() { | |
| const allChaptersRadio = document.getElementById('allChapters'); | |
| const selectedChaptersRadio = document.getElementById('selectedChapters'); | |
| const chaptersContainer = document.getElementById('chaptersSelectionContainer'); | |
| if (allChaptersRadio && selectedChaptersRadio && chaptersContainer) { | |
| // Handle radio button changes | |
| allChaptersRadio.addEventListener('change', function() { | |
| chaptersContainer.style.display = 'none'; | |
| }); | |
| selectedChaptersRadio.addEventListener('change', function() { | |
| chaptersContainer.style.display = 'block'; | |
| }); | |
| } | |
| } | |
| </script> | |
| <footer class="mt-4" id="mainFooter">© 2025 ICFAI Group</footer> | |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> | |
| <!-- Backend + Navigation Scripts --> | |
| <script> | |
| const backendBase = ""; | |
| // const backendBase = "https://transpulmonary-divergently-alfreda.ngrok-free.dev"; | |
| // function login() { | |
| // const user = document.getElementById("username").value.trim(); | |
| // const pass = document.getElementById("password").value.trim(); | |
| // const VALID_USER = "admin"; | |
| // const VALID_PASS = "icfai@123"; | |
| // if (user === VALID_USER && pass === VALID_PASS) { | |
| // document.getElementById("loginPage").style.display = "none"; | |
| // document.getElementById("mainPage").style.display = "flex"; | |
| // document.getElementById("mainHeader").style.display = "flex"; | |
| // document.getElementById("mainFooter").style.display = "block"; | |
| // } else { | |
| // document.getElementById("loginError").style.display = "block"; | |
| // } | |
| // } | |
| // function logout() { | |
| // document.getElementById("mainPage").style.display = "none"; | |
| // document.getElementById("mainHeader").style.display = "none"; | |
| // document.getElementById("mainFooter").style.display = "none"; | |
| // document.getElementById("loginPage").style.display = "flex"; | |
| // document.getElementById("username").value = ""; | |
| // document.getElementById("password").value = ""; | |
| // document.getElementById("loginError").style.display = "none"; | |
| // } | |
| // Navigation | |
| document.querySelectorAll('.nav-vertical .btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const pageId = this.getAttribute('data-page'); | |
| if (pageId) { | |
| document.querySelectorAll('.page').forEach(page => page.style.display = 'none'); | |
| document.getElementById(pageId).style.display = 'block'; | |
| // Load questions when questions page is opened | |
| if (pageId === 'questionsPage') { | |
| // Get current search values | |
| const searchInput = document.getElementById('searchInput'); | |
| const questionTypeSelect = document.getElementById('questionTypeSelect'); | |
| const currentSearch = searchInput ? searchInput.value : ''; | |
| const currentType = questionTypeSelect ? questionTypeSelect.value : 'all'; | |
| // Load questions with current filters | |
| loadQuestionsFromDB(currentSearch, currentType); | |
| loadTopics(); | |
| initializeSearchFunctionality(); // Initialize search functionality | |
| } | |
| if (pageId === 'generatePaperPage') { | |
| loadChaptersForPaper(); | |
| initializeChapterSelection(); | |
| } | |
| } | |
| }); | |
| }); | |
| </script> | |
| <!-- Questions Page Functionality --> | |
| <script> | |
| async function loadQuestionsFromDB(searchTerm = '', questionType = '') { | |
| console.log('loadQuestionsFromDB called with:', { searchTerm, questionType }); | |
| const questionsTable = document.getElementById('questionsTable'); | |
| const questionsCount = document.getElementById('questionsCount'); | |
| questionsTable.innerHTML = '<div class="text-center"><div class="spinner-border" role="status"></div><p>Loading questions...</p></div>'; | |
| try { | |
| let url = `${backendBase}/questions`; | |
| const params = new URLSearchParams(); | |
| console.log('Before params - searchTerm:', searchTerm, 'questionType:', questionType); | |
| if (searchTerm && searchTerm.trim() !== '') { | |
| params.append('search', searchTerm.trim()); | |
| } | |
| if (questionType && questionType !== 'all') { | |
| params.append('qtype', questionType); | |
| } | |
| console.log('Params:', params.toString()); | |
| if (params.toString()) { | |
| url += '?' + params.toString(); | |
| } | |
| console.log('Final URL:', url); | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const questions = await response.json(); | |
| // Update count | |
| questionsCount.innerHTML = `<div class="alert alert-info">Found ${questions.length} questions</div>`; | |
| if (questions.length === 0) { | |
| questionsTable.innerHTML = '<div class="alert alert-warning text-center">No questions found in the database.</div>'; | |
| return; | |
| } | |
| // Create table with editable fields | |
| let tableHTML = ` | |
| <div style="max-height: 500px; overflow-y: auto;"> | |
| <table class="table table-striped table-hover"> | |
| <thead class="table-light sticky-top"> | |
| <tr> | |
| <th>ID</th> | |
| <th>Topic</th> | |
| <th>Type</th> | |
| <th>Question</th> | |
| <th>Options/Answer</th> | |
| <th>Difficulty</th> | |
| <th>Flagged</th> | |
| <th>Actions</th> | |
| <th>Created</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| `; | |
| questions.forEach(q => { | |
| const createdDate = q.created_at ? new Date(q.created_at).toLocaleDateString() : 'N/A'; | |
| let optionsHtml = ''; | |
| if (q.type === 'MCQ') { | |
| optionsHtml = ` | |
| <div><strong>A:</strong> <span class="editable" contenteditable="true" data-field="option_a" data-id="${q.id}">${q.option_a || ''}</span></div> | |
| <div><strong>B:</strong> <span class="editable" contenteditable="true" data-field="option_b" data-id="${q.id}">${q.option_b || ''}</span></div> | |
| <div><strong>C:</strong> <span class="editable" contenteditable="true" data-field="option_c" data-id="${q.id}">${q.option_c || ''}</span></div> | |
| <div><strong>D:</strong> <span class="editable" contenteditable="true" data-field="option_d" data-id="${q.id}">${q.option_d || ''}</span></div> | |
| <div class="text-success"><strong>Answer:</strong> <span class="editable" contenteditable="true" data-field="answer" data-id="${q.id}">${q.answer || ''}</span></div> | |
| `; | |
| } else { | |
| optionsHtml = ` | |
| <div class="text-success"> | |
| <strong>Answer:</strong> | |
| <span class="editable" contenteditable="true" data-field="descriptive_answer" data-id="${q.id}"> | |
| ${q.descriptive_answer || 'Not provided'} | |
| </span> | |
| </div> | |
| `; | |
| } | |
| // Determine flagged status display | |
| const flaggedStatus = q.flagged === true ? | |
| '<span class="badge bg-success">Approved</span>' : | |
| q.flagged === false ? | |
| '<span class="badge bg-danger">Not Approved</span>' : | |
| '<span class="badge bg-secondary">Pending</span>'; | |
| tableHTML += ` | |
| <tr class="question-row ${(q.type || '').toLowerCase()}"> | |
| <td>${q.id}</td> | |
| <td> | |
| <span class="editable" contenteditable="true" data-field="topic" data-id="${q.id}"> | |
| ${q.topic || 'N/A'} | |
| </span> | |
| </td> | |
| <td><span class="badge ${q.type === 'MCQ' ? 'bg-success' : 'bg-warning'}">${q.type || 'Unknown'}</span></td> | |
| <td> | |
| <span class="editable" contenteditable="true" data-field="question" data-id="${q.id}"> | |
| ${q.question || ''} | |
| </span> | |
| </td> | |
| <td>${optionsHtml}</td> | |
| <td> | |
| <select class="form-select form-select-sm difficulty-select" data-id="${q.id}"> | |
| <option value="">—</option> | |
| ${[1,2,3,4,5].map(i => `<option value="${i}" ${q.difficulty == i ? 'selected' : ''}>${i}</option>`).join('')} | |
| </select> | |
| </td> | |
| <td>${flaggedStatus}</td> | |
| <td> | |
| <div class="btn-group btn-group-sm"> | |
| <button class="btn btn-success approve-btn" data-id="${q.id}" title="Approve Question" ${q.flagged === true ? 'disabled' : ''}> | |
| ✓ | |
| </button> | |
| <button class="btn btn-danger reject-btn" data-id="${q.id}" title="Reject Question" ${q.flagged === false ? 'disabled' : ''}> | |
| ✗ | |
| </button> | |
| </div> | |
| </td> | |
| <td>${createdDate}</td> | |
| </tr> | |
| `; | |
| }); | |
| tableHTML += '</tbody></table></div>'; | |
| questionsTable.innerHTML = tableHTML; | |
| // Initialize event listeners for the new table | |
| initializeQuestionTableEventListeners(); | |
| } catch (error) { | |
| console.error('Error loading questions:', error); | |
| questionsTable.innerHTML = '<div class="alert alert-danger">Error loading questions from database: ' + error.message + '</div>'; | |
| } | |
| } | |
| // Function to update individual question field | |
| async function updateQuestionField(questionId, field, value) { | |
| try { | |
| const response = await fetch(`${backendBase}/update_question`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| id: parseInt(questionId), | |
| updates: { | |
| [field]: value | |
| } | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| console.log(`✅ Question ${questionId} ${field} updated successfully`); | |
| showNotification(`Question ${field} updated successfully`, 'success'); | |
| return true; | |
| } else { | |
| console.error('❌ Failed to update question:', data.error); | |
| showNotification(`Failed to update question: ${data.error}`, 'error'); | |
| return false; | |
| } | |
| } catch (error) { | |
| console.error('❌ Error updating question:', error); | |
| showNotification('Error updating question. Please try again.', 'error'); | |
| return false; | |
| } | |
| } | |
| // Function to update question flag (approve/reject) | |
| async function updateQuestionFlag(questionId, flagged) { | |
| try { | |
| const response = await fetch(`${backendBase}/update_question`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| id: parseInt(questionId), | |
| updates: { | |
| flagged: flagged | |
| } | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| // Reload the questions to reflect the change | |
| loadQuestionsFromDB(); | |
| } else { | |
| console.error('Failed to update question flag:', data.error); | |
| alert('Failed to update question status. Please try again.'); | |
| } | |
| } catch (error) { | |
| console.error('Error updating question flag:', error); | |
| alert('Error updating question status. Please try again.'); | |
| } | |
| } | |
| // debounce helper | |
| function debounce(fn, wait = 300) { | |
| let t; | |
| return function(...args) { | |
| clearTimeout(t); | |
| t = setTimeout(() => fn.apply(this, args), wait); | |
| }; | |
| } | |
| // ensure the search input triggers filtered loads on typing | |
| (function wireLiveSearch() { | |
| const searchInput = document.getElementById('searchInput'); | |
| const questionTypeSelect = document.getElementById('questionTypeSelect'); | |
| const searchBtn = document.getElementById('searchBtn'); | |
| if (!searchInput) return; | |
| const doSearch = () => { | |
| const q = searchInput.value ? searchInput.value.trim() : ''; | |
| const type = (questionTypeSelect && questionTypeSelect.value) ? questionTypeSelect.value : 'all'; | |
| loadQuestionsFromDB(q, type); | |
| }; | |
| // live search on input with debounce | |
| searchInput.addEventListener('input', debounce(doSearch, 300)); | |
| // also update when question type changes | |
| if (questionTypeSelect) { | |
| questionTypeSelect.addEventListener('change', () => { | |
| // run with current search text | |
| const q = searchInput.value ? searchInput.value.trim() : ''; | |
| loadQuestionsFromDB(q, questionTypeSelect.value); | |
| }); | |
| } | |
| // keep the manual search button (if present) wired | |
| if (searchBtn) { | |
| searchBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| doSearch(); | |
| }); | |
| } | |
| })(); | |
| </script> | |
| <!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> --> | |
| <script> | |
| function displayGeneratedPaper(questions, targetElementId = 'generatedPaperResults') { | |
| const resultsDiv = document.getElementById(targetElementId); | |
| if (!resultsDiv) { | |
| console.error('Target element not found:', targetElementId); | |
| return; | |
| } | |
| resultsDiv.innerHTML = ''; | |
| if (!questions || questions.length === 0) { | |
| resultsDiv.innerHTML = '<div class="alert alert-warning">No questions found.</div>'; | |
| return; | |
| } | |
| // Shuffle MCQ options | |
| const processedQuestions = questions.map(q => { | |
| if (q.type === 'MCQ') { | |
| return shuffleMCQOptions(q); | |
| } | |
| return q; | |
| }); | |
| const mcqs = processedQuestions.filter(q => (q.type || '').toUpperCase() === 'MCQ'); | |
| const descs = processedQuestions.filter(q => (q.type || '').toUpperCase() !== 'MCQ'); | |
| const paperContainer = document.createElement('div'); | |
| paperContainer.className = 'question-paper'; | |
| const header = document.createElement('h5'); | |
| header.className = 'mb-3 text-primary'; | |
| header.textContent = `Generated Question Paper (${questions.length} questions) - OPTIONS SHUFFLED`; | |
| paperContainer.appendChild(header); | |
| // If there are MCQs, render them first | |
| if (mcqs.length) { | |
| const mcqSection = document.createElement('div'); | |
| mcqSection.className = 'mb-3'; | |
| mcqSection.innerHTML = `<h6>Multiple Choice Questions (${mcqs.length}) - Options Shuffled</h6>`; | |
| const table = document.createElement('table'); | |
| table.className = 'table table-bordered table-striped question-paper w-100 mb-3'; | |
| table.style.tableLayout = 'fixed'; | |
| let tableHTML = `<thead class="table-dark"><tr><th width="5%">#</th><th width="60%">Question</th><th width="35%">Options</th></tr></thead><tbody>`; | |
| mcqs.forEach((q, i) => { | |
| // Use the already formatted options with new labels | |
| const cleanA = cleanOptionText(q.option_a || ''); | |
| const cleanB = cleanOptionText(q.option_b || ''); | |
| const cleanC = cleanOptionText(q.option_c || ''); | |
| const cleanD = cleanOptionText(q.option_d || ''); | |
| tableHTML += `<tr><td><strong>${i+1}</strong></td><td>${q.question}</td><td> | |
| <div><strong>A:</strong> ${cleanA}</div> | |
| <div><strong>B:</strong> ${cleanB}</div> | |
| <div><strong>C:</strong> ${cleanC}</div> | |
| <div><strong>D:</strong> ${cleanD}</div> | |
| </td></tr>`; | |
| }); | |
| tableHTML += '</tbody>'; | |
| table.innerHTML = tableHTML; | |
| mcqSection.appendChild(table); | |
| paperContainer.appendChild(mcqSection); | |
| } | |
| // Rest of your display code remains the same... | |
| // Then descriptive questions | |
| if (descs.length) { | |
| const descSection = document.createElement('div'); | |
| descSection.className = 'mb-3'; | |
| descSection.innerHTML = `<h6>Descriptive / Short-answer Questions (${descs.length})</h6>`; | |
| const table = document.createElement('table'); | |
| table.className = 'table table-bordered table-striped question-paper w-100'; | |
| table.style.tableLayout = 'fixed'; | |
| let tableHTML = `<thead class="table-dark"><tr><th width="5%">#</th><th width="95%">Question</th></tr></thead><tbody>`; | |
| descs.forEach((q, i) => { | |
| tableHTML += `<tr><td><strong>${i+1}</strong></td><td>${q.question}</td></tr>`; | |
| }); | |
| tableHTML += '</tbody>'; | |
| table.innerHTML = tableHTML; | |
| descSection.appendChild(table); | |
| paperContainer.appendChild(descSection); | |
| } | |
| // Download button | |
| const downloadBtn = document.createElement('button'); | |
| downloadBtn.className = 'btn btn-success mt-3'; | |
| downloadBtn.innerHTML = '📥 Download Question Paper'; | |
| downloadBtn.onclick = () => downloadQuestionPaper(processedQuestions); | |
| paperContainer.appendChild(downloadBtn); | |
| resultsDiv.appendChild(paperContainer); | |
| } | |
| // Function to shuffle an array (Fisher-Yates algorithm) | |
| function shuffleArray(array) { | |
| const shuffled = [...array]; | |
| for (let i = shuffled.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; | |
| } | |
| return shuffled; | |
| } | |
| // Function to remove existing option labels (A., B., C., D.) | |
| function removeOptionLabel(optionText) { | |
| if (!optionText) return ''; | |
| // Remove patterns like "A. ", "B) ", "C:", "D - " etc. | |
| return optionText.replace(/^[A-D][\.\:\-\)]\s*/, '').trim(); | |
| } | |
| // Function to shuffle MCQ options while preserving the correct answer | |
| function shuffleMCQOptions(question) { | |
| if (question.type !== 'MCQ') return question; | |
| // Create options with cleaned text (without labels) | |
| const options = [ | |
| { label: 'A', text: removeOptionLabel(question.option_a || '') }, | |
| { label: 'B', text: removeOptionLabel(question.option_b || '') }, | |
| { label: 'C', text: removeOptionLabel(question.option_c || '') }, | |
| { label: 'D', text: removeOptionLabel(question.option_d || '') } | |
| ].filter(opt => opt.text.trim() !== ''); // Remove empty options | |
| // Shuffle the options | |
| const shuffledOptions = shuffleArray(options); | |
| // Find the new position of the correct answer | |
| const originalAnswer = question.answer; | |
| let newAnswer = ''; | |
| shuffledOptions.forEach((option, index) => { | |
| if (option.label === originalAnswer) { | |
| newAnswer = ['A', 'B', 'C', 'D'][index]; | |
| } | |
| }); | |
| // Create the shuffled question with new labels | |
| return { | |
| ...question, | |
| option_a: `A. ${shuffledOptions[0]?.text || ''}`, | |
| option_b: `B. ${shuffledOptions[1]?.text || ''}`, | |
| option_c: `C. ${shuffledOptions[2]?.text || ''}`, | |
| option_d: `D. ${shuffledOptions[3]?.text || ''}`, | |
| answer: newAnswer, | |
| original_answer: originalAnswer, // Keep original for reference | |
| options_shuffled: true // Flag to indicate options were shuffled | |
| }; | |
| } | |
| </script> | |
| <!-- PDF Logic --> | |
| <script> | |
| async function uploadAndExtractTOC(file) { | |
| const fd = new FormData(); | |
| fd.append("file", file, file.name); | |
| const res = await fetch(`${backendBase}/extract_toc`, { method: "POST", body: fd }); | |
| return res.json(); | |
| } | |
| // PDF UI wiring | |
| (function initPdf() { | |
| const pdfInput = document.querySelector('#pdfPage #pdfUploader'); | |
| const pdfBox = pdfInput && pdfInput.closest('.upload-box'); | |
| const statNodes = Array.from(document.querySelectorAll('#pdfPage .stats-card h3') || []); | |
| function updateDashboardFromResponse(resp) { | |
| try { | |
| if (!resp) return; | |
| if (statNodes[0]) statNodes[0].textContent = (resp.global_state?.pdf_uploads ?? statNodes[0].textContent); | |
| if (statNodes[1]) statNodes[1].textContent = (typeof resp.pages === 'number' ? resp.pages : (resp.global_state?.last_pdf_pages ?? statNodes[1].textContent)); | |
| if (statNodes[2]) statNodes[2].textContent = (typeof resp.mcqCount === 'number') ? resp.mcqCount : statNodes[2].textContent; | |
| if (statNodes[3]) statNodes[3].textContent = (typeof resp.descCount === 'number') ? resp.descCount : statNodes[3].textContent; | |
| } catch (e) { | |
| console.warn("Failed to update dashboard:", e); | |
| } | |
| } | |
| if (!pdfInput || !pdfBox) return; | |
| // Build controls area under upload box | |
| const ctrlWrap = document.createElement('div'); | |
| ctrlWrap.className = 'mt-3'; | |
| pdfBox.appendChild(ctrlWrap); | |
| const statusEl = document.createElement('div'); | |
| statusEl.style.marginTop = '8px'; | |
| ctrlWrap.appendChild(statusEl); | |
| const tocEl = document.createElement('div'); | |
| tocEl.style.marginTop = '8px'; | |
| ctrlWrap.appendChild(tocEl); | |
| const optionsForm = document.createElement('div'); | |
| optionsForm.innerHTML = ` | |
| <div class="row g-2 align-items-center"> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0">Scope</label> | |
| <div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="radio" name="scopeRadio" id="scopeAll" value="all" checked> | |
| <label class="form-check-label small" for="scopeAll">All topics</label> | |
| </div> | |
| <div class="form-check form-check-inline"> | |
| <input class="form-check-input" type="radio" name="scopeRadio" id="scopeSelected" value="selected"> | |
| <label class="form-check-label small" for="scopeSelected">Selected chapters only</label> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0">MCQ source</label> | |
| <div> | |
| <select id="mcqSource" class="form-select form-select-sm"> | |
| <option value="llama_open">Llama3 (open-ended)</option> | |
| <option value="textbook">From textbook (use chapter content)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0">Question type</label> | |
| <div> | |
| <select id="questionType" class="form-select form-select-sm"> | |
| <option value="mcq">MCQs</option> | |
| <option value="descriptive">Descriptive</option> | |
| <option value="both" selected>Both</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0 small">MCQs per topic</label> | |
| <input id="numMcqs" type="number" min="1" max="50" value="5" class="form-control form-control-sm" style="width:80px;"> | |
| </div> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0 small">Descriptive per topic</label> | |
| <input id="numDesc" type="number" min="0" max="20" value="3" class="form-control form-control-sm" style="width:80px;"> | |
| </div> | |
| <div class="col-auto"> | |
| <button id="generateBtn" class="btn btn-primary btn-sm" style="margin-top:6px; display:none;">Generate MCQs</button> | |
| </div> | |
| </div> | |
| `; | |
| ctrlWrap.appendChild(optionsForm); | |
| const chaptersContainer = document.createElement('div'); | |
| chaptersContainer.className = 'mt-3'; | |
| ctrlWrap.appendChild(chaptersContainer); | |
| const resultsArea = document.createElement('div'); | |
| resultsArea.className = 'mt-3'; | |
| pdfBox.appendChild(resultsArea); | |
| let lastMatches = []; | |
| pdfInput.addEventListener('change', async (e) => { | |
| resultsArea.innerHTML = ''; | |
| chaptersContainer.innerHTML = ''; | |
| tocEl.innerHTML = ''; | |
| const f = e.target.files[0]; | |
| if (!f) return; | |
| statusEl.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Parsing TOC...'; | |
| try { | |
| const r = await uploadAndExtractTOC(f); | |
| if (r.status === 'success') { | |
| statusEl.innerHTML = `<div class="small text-success">Found ${r.matches?.length || 0} TOC entries across ${r.chapters_count || 0} chapters.</div>`; | |
| lastMatches = r.matches || []; | |
| // Build chapter groups | |
| const groups = {}; | |
| (lastMatches || []).forEach(m => { | |
| const chap = (m.subnum || '0').split('.')[0]; | |
| groups[chap] = groups[chap] || []; | |
| groups[chap].push(m); | |
| }); | |
| // Show chapter checkboxes | |
| chaptersContainer.innerHTML = '<div class="small fw-bold mb-2">Chapters (pick for "Selected chapters only")</div>'; | |
| const grid = document.createElement('div'); | |
| grid.className = 'row g-2'; | |
| Object.keys(groups).sort((a,b)=>parseInt(a)-parseInt(b)).forEach(chap => { | |
| const col = document.createElement('div'); | |
| col.className = 'col-6 col-md-3'; | |
| const card = document.createElement('div'); | |
| card.className = 'p-2 border rounded small'; | |
| const chkId = `chap_chk_${chap}`; | |
| card.innerHTML = `<div class="form-check"><input class="form-check-input chap-chk" type="checkbox" value="${chap}" id="${chkId}"><label class="form-check-label" for="${chkId}">Chapter ${chap} • ${groups[chap].length} topics</label></div>`; | |
| col.appendChild(card); | |
| grid.appendChild(col); | |
| }); | |
| chaptersContainer.appendChild(grid); | |
| tocEl.innerHTML = `<div class="small">Detected ${lastMatches.length} TOC entries across ${Object.keys(groups).length} chapters — Pages detected: ${r.pages ?? '-'}</div>`; | |
| document.getElementById('generateBtn').style.display = 'inline-block'; | |
| } else { | |
| statusEl.textContent = 'TOC parse failed: ' + (r.error || 'unknown'); | |
| document.getElementById('generateBtn').style.display = 'none'; | |
| } | |
| } catch (err) { | |
| statusEl.textContent = 'Error: ' + err; | |
| document.getElementById('generateBtn').style.display = 'none'; | |
| } | |
| }); | |
| ctrlWrap.addEventListener('change', (ev) => { | |
| const scope = ctrlWrap.querySelector('input[name="scopeRadio"]:checked').value; | |
| chaptersContainer.style.display = (scope === 'selected') ? '' : 'none'; | |
| }); | |
| chaptersContainer.style.display = 'none'; | |
| document.getElementById('generateBtn').addEventListener('click', async () => { | |
| resultsArea.innerHTML = ''; | |
| const f = pdfInput.files[0]; | |
| if (!f) return alert('Choose a PDF first'); | |
| const scope = ctrlWrap.querySelector('input[name="scopeRadio"]:checked').value; | |
| const selectedChapters = []; | |
| if (scope === 'selected') { | |
| document.querySelectorAll('.chap-chk:checked').forEach(c => selectedChapters.push(parseInt(c.value))); | |
| if (!selectedChapters.length) return alert('Select at least one chapter or choose "All topics"'); | |
| } | |
| const qType = document.getElementById('questionType').value; | |
| const mcqSource = document.getElementById('mcqSource').value; | |
| const numMcqs = parseInt(document.getElementById('numMcqs').value || '5', 10); | |
| const numDesc = parseInt(document.getElementById('numDesc').value || '3', 10); | |
| const btn = document.getElementById('generateBtn'); | |
| btn.disabled = true; | |
| btn.textContent = 'Generating…'; | |
| try { | |
| const fd = new FormData(); | |
| fd.append("file", f, f.name); | |
| fd.append("chapters", JSON.stringify(selectedChapters || [])); | |
| fd.append("question_type", qType); | |
| fd.append("mcq_source", mcqSource); | |
| fd.append("num_mcqs", String(numMcqs)); | |
| fd.append("num_desc", String(numDesc)); | |
| const response = await fetch(`${backendBase}/generate_pdf_mcqs`, { method: "POST", body: fd }); | |
| const r = await response.json(); | |
| if (r.status === 'success') { | |
| // Update dashboard counts | |
| updateDashboardFromResponse(r); | |
| // Render results | |
| const results = r.results || {}; | |
| const header = document.createElement('div'); | |
| header.className='mb-2'; | |
| header.innerHTML = `<div class="small"><b>Generated for ${Object.keys(results).length} topics</b> — MCQs: ${r.mcqCount || 0} — Descriptive: ${r.descCount || 0}</div>`; | |
| resultsArea.appendChild(header); | |
| // Downloads | |
| const dlRow = document.createElement('div'); | |
| dlRow.className='mb-3'; | |
| const keys = r.download_keys || {}; | |
| if (keys.docx) { | |
| const a = document.createElement('a'); | |
| a.href = `${backendBase}/download/${keys.docx}`; | |
| a.className='btn btn-outline-primary btn-sm me-2'; | |
| a.textContent = 'Download DOCX'; | |
| dlRow.appendChild(a); | |
| } | |
| if (keys.excel) { | |
| const a = document.createElement('a'); | |
| a.href = `${backendBase}/download/${keys.excel}`; | |
| a.className='btn btn-outline-success btn-sm me-2'; | |
| a.textContent = 'Download Excel'; | |
| dlRow.appendChild(a); | |
| } | |
| if (keys.csv) { | |
| const a = document.createElement('a'); | |
| a.href = `${backendBase}/download/${keys.csv}`; | |
| a.className='btn btn-outline-secondary btn-sm me-2'; | |
| a.textContent = 'Download CSV'; | |
| dlRow.appendChild(a); | |
| } | |
| // Create Export to Database button | |
| const exportPdfBtn = document.createElement('button'); | |
| exportPdfBtn.className = 'btn btn-warning btn-sm me-2'; | |
| exportPdfBtn.innerHTML = '📤 Export to Database'; | |
| exportPdfBtn.addEventListener('click', async () => { | |
| exportPdfBtn.disabled = true; | |
| exportPdfBtn.textContent = 'Exporting…'; | |
| try { | |
| const payload = results; | |
| const resp = await fetch(`${backendBase}/save_questions_to_db`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const j = await resp.json().catch(() => ({status: 'error', error: 'invalid_json'})); | |
| if (j.status === 'success') { | |
| alert('✅ PDF questions exported to database.'); | |
| } else { | |
| alert('❌ Export failed: ' + (j.error || JSON.stringify(j))); | |
| } | |
| } catch (err) { | |
| alert('⚠ Error exporting PDF questions: ' + err); | |
| } finally { | |
| exportPdfBtn.disabled = false; | |
| exportPdfBtn.innerHTML = '📤 Export to Database'; | |
| } | |
| }); | |
| dlRow.appendChild(exportPdfBtn); | |
| resultsArea.appendChild(dlRow); | |
| const topicsWrap = document.createElement('div'); topicsWrap.className='accordion'; resultsArea.appendChild(topicsWrap); | |
| const renderMCQ = (qType === 'mcq' || qType === 'both'); | |
| const renderDesc = (qType === 'descriptive' || qType === 'both'); | |
| Object.keys(results).forEach((topicTitle, idx) => { | |
| const block = results[topicTitle]; | |
| const mcqs = block.mcqs || []; | |
| const descrs = block.descriptive || []; | |
| const item = document.createElement('div'); item.className='accordion-item'; | |
| const hId = `heading${idx}`; | |
| const cId = `collapse${idx}`; | |
| item.innerHTML = ` | |
| <h2 class="accordion-header" id="${hId}"> | |
| <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#${cId}" aria-expanded="false" aria-controls="${cId}"> | |
| ${topicTitle} <small class="text-muted ms-2"> (MCQs: ${mcqs.length} • Descr: ${descrs.length})</small> | |
| </button> | |
| </h2> | |
| <div id="${cId}" class="accordion-collapse collapse" aria-labelledby="${hId}"> | |
| <div class="accordion-body"> | |
| <div class="result-content"></div> | |
| </div> | |
| </div> | |
| `; | |
| topicsWrap.appendChild(item); | |
| const contentDiv = item.querySelector('.result-content'); | |
| if (renderMCQ && mcqs && mcqs.length) { | |
| const mcqTitle = document.createElement('div'); mcqTitle.className='mb-2'; | |
| mcqTitle.innerHTML = `<b>Multiple Choice Questions</b>`; | |
| contentDiv.appendChild(mcqTitle); | |
| mcqs.forEach((m, mi) => { | |
| const qDiv = document.createElement('div'); qDiv.className = 'mb-2'; | |
| const qtext = m.question || `Q${mi+1}.`; | |
| const difficulty = m.difficulty || 'N/A'; | |
| qDiv.innerHTML = `<div><strong>${mi+1}. ${qtext}</strong> <small class="text-muted"> — Level ${difficulty}</small></div>`; | |
| const ul = document.createElement('ul'); ul.className='small'; | |
| (m.options || []).forEach(opt => { | |
| const li = document.createElement('li'); li.textContent = opt; | |
| ul.appendChild(li); | |
| }); | |
| qDiv.appendChild(ul); | |
| if (m.answer) { | |
| const ans = document.createElement('div'); ans.className='text-success small'; ans.textContent = `✅ Answer: ${m.answer}`; | |
| qDiv.appendChild(ans); | |
| } | |
| contentDiv.appendChild(qDiv); | |
| }); | |
| } | |
| if (renderDesc && descrs && descrs.length) { | |
| const descTitle = document.createElement('div'); descTitle.className='mt-3 mb-2'; | |
| descTitle.innerHTML = `<b>Descriptive / Short-answer Questions</b>`; | |
| contentDiv.appendChild(descTitle); | |
| descrs.forEach((d, di) => { | |
| const dDiv = document.createElement('div'); dDiv.className='mb-2'; | |
| const q = typeof d === 'string' ? d : (d.question || ''); | |
| const a = (typeof d === 'object' ? (d.answer || '') : ''); | |
| const diff = (typeof d === 'object' ? (d.difficulty || 'N/A') : 'N/A'); | |
| dDiv.innerHTML = `<div>${di+1}. ${q} <small class="text-muted"> — Level ${diff}</small></div>`; | |
| if (a) { | |
| const ans = document.createElement('div'); ans.className='text-success small'; ans.textContent = `✅ Answer: ${a}`; | |
| dDiv.appendChild(ans); | |
| } | |
| contentDiv.appendChild(dDiv); | |
| }); | |
| } | |
| }); | |
| function setFilter(mode) { | |
| Array.from(resultsArea.querySelectorAll('.result-content')).forEach(rc=>{ | |
| if (mode === 'mcq') { | |
| rc.querySelectorAll('div').forEach(div=>{ | |
| const txt = div.innerText || ''; | |
| if (txt.includes('Descriptive / Short-answer Questions')) div.style.display = 'none'; | |
| if (txt.includes('Multiple Choice Questions')) div.style.display = ''; | |
| if (!txt.includes('Descriptive / Short-answer Questions') && !txt.includes('Multiple Choice Questions')) div.style.display = ''; | |
| }); | |
| } else if (mode === 'des') { | |
| rc.querySelectorAll('div').forEach(div=>{ | |
| const txt = div.innerText || ''; | |
| if (txt.includes('Multiple Choice Questions')) div.style.display = 'none'; | |
| if (txt.includes('Descriptive / Short-answer Questions')) div.style.display = ''; | |
| if (!txt.includes('Descriptive / Short-answer Questions') && !txt.includes('Multiple Choice Questions')) div.style.display = ''; | |
| }); | |
| } else { | |
| rc.querySelectorAll('div').forEach(div=>div.style.display = ''); | |
| } | |
| }); | |
| } | |
| if (qType === 'mcq') setFilter('mcq'); | |
| else if (qType === 'descriptive') setFilter('des'); | |
| else setFilter('both'); | |
| } else { | |
| alert('Generation failed: ' + (r.error || 'unknown')); | |
| } | |
| } catch (err) { | |
| alert('Error: ' + err); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = 'Generate MCQs'; | |
| } | |
| }); | |
| })(); | |
| </script> | |
| <!-- === FULL VIDEO LOGIC (from original) === --> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // note: uses shared backendBase variable defined earlier | |
| const statNodes = Array.from(document.querySelectorAll('#videoPage .stats-card h3') || []); | |
| function updateDashboardFromResponse(resp) { | |
| try { | |
| if (!resp) return; | |
| // update video uploads | |
| //const vUploads = resp.global_state?.video_uploads; | |
| // video uploads might be at resp.global_state.video_uploads OR resp.video_uploads (top-level) | |
| const vUploads = (resp.global_state && typeof resp.global_state.video_uploads === 'number') | |
| ? resp.global_state.video_uploads | |
| : (typeof resp.video_uploads === 'number' ? resp.video_uploads : undefined); | |
| if (typeof vUploads === 'number') { | |
| const el = document.getElementById('videoUploadsStat'); | |
| if (el) el.textContent = vUploads; | |
| } | |
| // update MCQs generated (per-request or global) | |
| const mcqsNow = (typeof resp.mcqCount === 'number') ? resp.mcqCount : resp.global_state?.mcq_count; | |
| if (typeof mcqsNow === 'number') { | |
| const el2 = document.getElementById('videoMcqStat'); | |
| if (el2) el2.textContent = mcqsNow; | |
| } | |
| } catch (e) { | |
| console.warn("updateDashboardFromResponse failed", e); | |
| } | |
| } | |
| async function transcribeVideo(file, whisper_model="small") { | |
| const fd = new FormData(); | |
| fd.append("file", file, file.name); | |
| fd.append("whisper_model", whisper_model); | |
| const r = await fetch(`${backendBase}/transcribe_video`, { method: "POST", body: fd }); | |
| const json = await r.json().catch(()=>({status:"error", error:"invalid_json_response"})); | |
| return json; | |
| } | |
| async function generateVideoMCQs({file=null, summary="", questionType="both", numQs=10}) { | |
| const fd = new FormData(); | |
| if (file) fd.append("file", file, file.name); | |
| fd.append("summary", summary || ""); | |
| fd.append("question_type", questionType); | |
| fd.append("num_qs", String(numQs)); | |
| const r = await fetch(`${backendBase}/generate_video_mcqs`, { method: "POST", body: fd }); | |
| const json = await r.json().catch(()=>({status:"error", error:"invalid_json_response"})); | |
| return json; | |
| } | |
| // Find the video input (inside Video page) | |
| const videoInput = document.querySelector('#videoPage #videoUploader'); | |
| if (!videoInput) { | |
| console.warn("No video input found in #videoPage"); | |
| return; | |
| } | |
| const videoBox = videoInput.closest('.upload-box') || document.body; | |
| // build controls | |
| const ctrl = document.createElement('div'); ctrl.className = 'mt-3'; | |
| const transBtn = document.createElement('button'); transBtn.className = 'btn btn-outline-primary btn-sm me-2'; | |
| transBtn.textContent = 'Transcribe'; | |
| transBtn.disabled = true; | |
| const genBtn = document.createElement('button'); genBtn.className = 'btn btn-primary btn-sm'; | |
| genBtn.textContent = 'Generate MCQs'; | |
| genBtn.disabled = true; | |
| ctrl.appendChild(transBtn); ctrl.appendChild(genBtn); | |
| videoBox.appendChild(ctrl); | |
| const status = document.createElement('div'); status.className = 'mt-2 small'; status.style.whiteSpace = 'pre-wrap'; | |
| videoBox.appendChild(status); | |
| const transcriptArea = document.createElement('textarea'); transcriptArea.className='form-control mt-2'; | |
| transcriptArea.rows = 8; transcriptArea.placeholder = 'Transcript will appear here (or paste one)...'; | |
| videoBox.appendChild(transcriptArea); | |
| const summaryDiv = document.createElement('div'); summaryDiv.className='mt-2'; | |
| videoBox.appendChild(summaryDiv); | |
| const chunksDiv = document.createElement('div'); chunksDiv.className='mt-2 small text-muted'; | |
| videoBox.appendChild(chunksDiv); | |
| const opts = document.createElement('div'); opts.className='mt-2'; | |
| opts.innerHTML = ` | |
| <div class="row g-2 align-items-center"> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0 small">Question type</label> | |
| <select id="videoQuestionType" class="form-select form-select-sm"> | |
| <option value="mcq">MCQs</option> | |
| </select> | |
| </div> | |
| <div class="col-auto"> | |
| <label class="form-label mb-0 small">Number of MCQs</label> | |
| <input id="videoNumQs" class="form-control form-control-sm" value="10" /> | |
| </div> | |
| </div> | |
| `; | |
| videoBox.appendChild(opts); | |
| const resultsWrap = document.createElement('div'); resultsWrap.className='mt-3'; | |
| videoBox.appendChild(resultsWrap); | |
| // enable buttons when user selects a file | |
| videoInput.addEventListener('change', (e) => { | |
| const f = e.target.files && e.target.files[0]; | |
| if (f) { | |
| transBtn.disabled = false; | |
| genBtn.disabled = false; | |
| status.textContent = `Selected file: ${f.name} (${Math.round(f.size/1024)} KB)`; | |
| console.log("Video selected", f); | |
| } else { | |
| transBtn.disabled = true; | |
| genBtn.disabled = true; | |
| } | |
| }); | |
| // Transcribe button | |
| transBtn.addEventListener('click', async () => { | |
| const f = videoInput.files && videoInput.files[0]; | |
| if (!f) return alert('Choose a video file first.'); | |
| status.textContent = 'Transcribing... (may take minutes)'; | |
| transBtn.disabled = true; genBtn.disabled = true; | |
| try { | |
| const r = await transcribeVideo(f, "small"); | |
| if (r.status === 'success') { | |
| status.textContent = 'Transcription complete.'; | |
| transcriptArea.value = r.transcript || ''; | |
| summaryDiv.innerHTML = `<b>Summary</b><div class="small mt-1">${(r.summary || '').replace(/\n/g,'<br>')}</div>`; | |
| if (r.chunks && r.chunks.length) { | |
| chunksDiv.innerHTML = '<b>Chunk summaries:</b><br>' + r.chunks.slice(0,10).map((c,i)=>`<div>● Chunk ${i+1}: ${c}</div>`).join(''); | |
| } else chunksDiv.innerHTML = ''; | |
| genBtn.disabled = false; | |
| if (r.global_state) updateDashboardFromResponse(r); | |
| } else { | |
| status.textContent = 'Transcription error: ' + (r.error || 'unknown'); | |
| console.warn("transcribe error", r); | |
| } | |
| } catch (err) { | |
| status.textContent = 'Network / JS error during transcription: ' + err; | |
| console.error(err); | |
| } finally { | |
| transBtn.disabled = false; | |
| } | |
| }); | |
| // Generate button | |
| genBtn.addEventListener('click', async () => { | |
| const f = videoInput.files && videoInput.files[0]; | |
| const qType = document.getElementById('videoQuestionType').value; | |
| const numQs = parseInt(document.getElementById('videoNumQs').value || '10', 10); | |
| resultsWrap.innerHTML = ''; | |
| status.textContent = 'Generating questions...'; | |
| genBtn.disabled = true; transBtn.disabled = true; | |
| try { | |
| const curSummary = summaryDiv.innerText.trim() ? summaryDiv.innerText.trim() : (transcriptArea.value.trim() ? "" : ""); | |
| let response; | |
| if (curSummary) { | |
| response = await generateVideoMCQs({ file: null, summary: curSummary, questionType: qType, numQs: numQs }); | |
| } else { | |
| if (!f) return alert('No file selected and no summary available.'); | |
| response = await generateVideoMCQs({ file: f, summary: "", questionType: qType, numQs: numQs }); | |
| } | |
| if (response.status === 'success') { | |
| status.textContent = `Generated — MCQs: ${response.mcqCount || 0} `; | |
| updateDashboardFromResponse(response); | |
| // downloads | |
| const keys = response.download_keys || {}; | |
| const dlRow = document.createElement('div'); dlRow.className = 'mb-2'; | |
| if (keys.docx) { | |
| const a = document.createElement('a'); a.href = `${backendBase}/download/${keys.docx}`; a.className='btn btn-outline-primary btn-sm me-2'; a.textContent='Download DOCX'; | |
| dlRow.appendChild(a); | |
| } | |
| if (keys.excel) { | |
| const a = document.createElement('a'); a.href = `${backendBase}/download/${keys.excel}`; a.className='btn btn-outline-success btn-sm me-2'; a.textContent='Download Excel'; | |
| dlRow.appendChild(a); | |
| } | |
| if (keys.csv) { | |
| const a = document.createElement('a'); a.href = `${backendBase}/download/${keys.csv}`; a.className='btn btn-outline-secondary btn-sm me-2'; a.textContent='Download CSV'; | |
| dlRow.appendChild(a); | |
| } | |
| resultsWrap.appendChild(dlRow); | |
| // render results per topic | |
| const res = response.results || {}; | |
| for (const topic of Object.keys(res)) { | |
| const block = res[topic]; | |
| if (block.mcqs && block.mcqs.length) { | |
| const h = document.createElement('div'); h.className='mt-2'; h.innerHTML = `<b>MCQs</b>`; | |
| resultsWrap.appendChild(h); | |
| block.mcqs.forEach((m,i) => { | |
| const div = document.createElement('div'); div.className='mb-2'; | |
| div.innerHTML = `<div><strong>${i+1}. ${m.question}</strong></div>`; | |
| const ul = document.createElement('ul'); ul.className='small'; | |
| (m.options || []).forEach(opt => { const li = document.createElement('li'); li.textContent = opt; ul.appendChild(li); }); | |
| div.appendChild(ul); | |
| if (m.answer) { const a = document.createElement('div'); a.className='text-success small'; a.textContent = `✅ Answer: ${m.answer}`; div.appendChild(a); } | |
| resultsWrap.appendChild(div); | |
| }); | |
| } | |
| if (block.descriptive && block.descriptive.length) { | |
| const h = document.createElement('div'); h.className='mt-3'; h.innerHTML = `<b>Descriptive</b>`; | |
| resultsWrap.appendChild(h); | |
| block.descriptive.forEach((d,i) => { | |
| const div = document.createElement('div'); div.className='mb-2'; | |
| const q = (typeof d === 'object') ? d.question : d; | |
| const a = (typeof d === 'object') ? d.answer : ''; | |
| const diff = (typeof d === 'object') ? d.difficulty : 'N/A'; | |
| div.innerHTML = `<div>${i+1}. ${q} <small class="text-muted"> — Level ${diff}</small></div>`; | |
| if (a) { const an = document.createElement('div'); an.className='text-success small'; an.textContent = `✅ Answer: ${a}`; div.appendChild(an); } | |
| resultsWrap.appendChild(div); | |
| }); | |
| } | |
| } | |
| } else { | |
| status.textContent = 'Generation error: ' + (response.error || 'unknown'); | |
| console.warn("generation error", response); | |
| } | |
| } catch (err) { | |
| console.error("generate exception", err); | |
| status.textContent = 'Network / JS error during generation: ' + err; | |
| } finally { | |
| genBtn.disabled = false; transBtn.disabled = false; | |
| } | |
| }); | |
| }); // DOMContentLoaded | |
| </script> | |
| <!-- NAV & HISTORY HANDLING (Back button -> main page) --> | |
| <script> | |
| (function setupNavHistory(){ | |
| const mainPage = document.getElementById('mainPage'); | |
| const pages = Array.from(document.querySelectorAll('.page')); | |
| function showPageById(id) { | |
| // hide all pages & main | |
| pages.forEach(p => p.style.display = 'none'); | |
| mainPage.style.display = 'none'; | |
| // show requested | |
| const el = document.getElementById(id); | |
| if (el) el.style.display = 'block'; | |
| } | |
| // add click handlers for nav buttons | |
| document.querySelectorAll('.nav-vertical .btn').forEach(btn => { | |
| btn.addEventListener('click', (ev) => { | |
| const pageId = btn.getAttribute('data-page'); | |
| if (!pageId) return; | |
| showPageById(pageId); | |
| // push state so back button works | |
| history.pushState({page: pageId}, "", "#" + pageId); | |
| }); | |
| }); | |
| // handle popstate (back/forward) | |
| window.addEventListener('popstate', (e) => { | |
| if (e.state && e.state.page) { | |
| showPageById(e.state.page); | |
| } else { | |
| // go home | |
| pages.forEach(p => p.style.display = 'none'); | |
| mainPage.style.display = 'flex'; | |
| } | |
| }); | |
| // initial load: if hash present, open that page (and push state) | |
| const initialHash = window.location.hash && window.location.hash.replace('#',''); | |
| if (initialHash && document.getElementById(initialHash)) { | |
| // show target and replace history state | |
| showPageById(initialHash); | |
| history.replaceState({page: initialHash}, "", "#" + initialHash); | |
| } else { | |
| // show main page | |
| pages.forEach(p => p.style.display = 'none'); | |
| mainPage.style.display = 'flex'; | |
| history.replaceState({}, "", window.location.pathname); | |
| } | |
| })(); | |
| </script> | |
| <script> | |
| // global function to show main page / hide other pages | |
| function goHome() { | |
| try { | |
| // hide all content pages | |
| document.querySelectorAll('.page').forEach(p => p.style.display = 'none'); | |
| // show main navigation | |
| const main = document.getElementById('mainPage'); | |
| if (main) main.style.display = 'flex'; | |
| // optionally show header/footer (if login state allows) | |
| const header = document.getElementById('mainHeader'); | |
| const footer = document.getElementById('mainFooter'); | |
| if (header) header.style.display = header.style.display === 'none' ? 'flex' : header.style.display; | |
| if (footer) footer.style.display = footer.style.display === 'none' ? 'block' : footer.style.display; | |
| // push history state so back button works | |
| history.pushState({ page: 'home' }, "", "#"); | |
| } catch (e) { | |
| console.warn("goHome error", e); | |
| } | |
| } | |
| // handle browser back/forward: if user navigates back to empty state, show main page | |
| window.addEventListener('popstate', function (e) { | |
| if (!e.state || e.state.page === 'home') { | |
| document.querySelectorAll('.page').forEach(p => p.style.display = 'none'); | |
| const main = document.getElementById('mainPage'); | |
| if (main) main.style.display = 'flex'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |