Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Edit NeetPrep Questions{% endblock %} | |
| {% block head %} | |
| <style> | |
| /* Style for rendered question HTML */ | |
| #topic_q_text img { | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 4px; | |
| } | |
| #topic_q_text p { | |
| margin-bottom: 0.5rem; | |
| } | |
| #topic_q_text { | |
| font-size: 0.95rem; | |
| line-height: 1.5; | |
| } | |
| /* Truncate table cell text */ | |
| .question-text-cell { | |
| max-width: 300px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| /* Range toggles */ | |
| .range-toggle { | |
| transition: all var(--transition-fast); | |
| border-radius: 50px; | |
| } | |
| .range-toggle:hover { | |
| transform: translateY(-1px); | |
| } | |
| .range-toggle.active { transform: scale(1.02); box-shadow: 0 0 8px rgba(255,255,255,0.2); } | |
| .range-toggle.active.btn-outline-primary { background: var(--accent-primary); color: #fff; } | |
| .range-toggle.active.btn-outline-warning { background: var(--accent-warning); color: #000; } | |
| .range-toggle.active.btn-outline-info { background: var(--accent-info); color: #000; } | |
| /* Range slider styling */ | |
| .range-slider-row { background: var(--bg-elevated); border-radius: 10px; padding: 12px; margin-bottom: 10px; transition: all var(--transition-fast); } | |
| .range-slider-row:hover { background: var(--bg-hover); } | |
| .dual-range-container { position: relative; height: 40px; } | |
| .dual-range-track { position: absolute; top: 50%; left: 0; right: 0; height: 8px; background: var(--border-subtle); border-radius: 4px; transform: translateY(-50%); } | |
| .dual-range-highlight { position: absolute; top: 50%; height: 8px; background: linear-gradient(90deg, var(--accent-primary), var(--accent-info)); border-radius: 4px; transform: translateY(-50%); } | |
| .dual-range-container input[type="range"] { position: absolute; top: 0; left: 0; width: 100%; height: 100%; -webkit-appearance: none; background: transparent; pointer-events: none; } | |
| .dual-range-container input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: #fff; border: 3px solid var(--accent-primary); border-radius: 50%; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); transition: transform var(--transition-fast); } | |
| .dual-range-container input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); } | |
| .dual-range-container input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: #fff; border: 3px solid var(--accent-primary); border-radius: 50%; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); } | |
| /* Card styling */ | |
| .edit-card { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .edit-card .card-header { | |
| background: linear-gradient(180deg, var(--bg-card), var(--bg-dark)); | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="container mt-5"> | |
| <div class="card bg-dark text-white edit-card"> | |
| <div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2"> | |
| <h2><i class="bi bi-pencil-square me-2"></i>Edit NeetPrep Questions</h2> | |
| <div class="d-flex gap-2"> | |
| <button type="button" class="btn btn-warning btn-pill" data-bs-toggle="modal" data-bs-target="#manualClassificationModal"> | |
| <i class="bi bi-list-check"></i> Manual Classification | |
| </button> | |
| <a href="/neetprep" class="btn btn-outline-light btn-pill">Back to NeetPrep Home</a> | |
| </div> | |
| </div> | |
| <div class="card-body"> | |
| <!-- Filter Form --> | |
| <div class="row mb-3"> | |
| <div class="col-md-6"> | |
| <input type="text" id="search-input" class="form-control bg-dark text-white" placeholder="Search questions..."> | |
| </div> | |
| <div class="col-md-4"> | |
| <select id="topic-filter" class="form-select bg-dark text-white"> | |
| <option value="all">All Topics</option> | |
| <option value="Unclassified" selected>Unclassified Only</option> | |
| {% for topic in topics %} | |
| {% if topic != 'Unclassified' %} | |
| <option value="{{ topic }}">{{ topic }}</option> | |
| {% endif %} | |
| {% endfor %} | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Questions Table --> | |
| <div class="table-responsive"> | |
| <table class="table table-dark table-hover"> | |
| <thead> | |
| <tr> | |
| <th style="width: 40px;">#</th> | |
| <th>Question Text</th> | |
| <th>Topic</th> | |
| <th>Subject</th> | |
| <th>Action</th> | |
| </tr> | |
| </thead> | |
| <tbody id="questions-table-body"> | |
| {% for q in questions %} | |
| <tr data-topic="{{ q.topic }}" data-id="{{ q.id }}" data-index="{{ loop.index }}" data-html="{{ q.question_text | e }}"> | |
| <td>{{ loop.index }}</td> | |
| <td class="question-text-cell">{{ q.question_text_plain[:100] }}{% if q.question_text_plain|length > 100 %}...{% endif %}</td> | |
| <td class="topic-cell">{{ q.topic }}</td> | |
| <td class="subject-cell">{{ q.subject }}</td> | |
| <td> | |
| <button class="btn btn-sm btn-primary edit-btn" | |
| data-id="{{ q.id }}" | |
| data-topic="{{ q.topic }}" | |
| data-subject="{{ q.subject }}" | |
| data-bs-toggle="modal" | |
| data-bs-target="#editModal"> | |
| Edit | |
| </button> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Edit Modal (Single Question) --> | |
| <div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header"> | |
| <h5 class="modal-title" id="editModalLabel">Edit Question Metadata</h5> | |
| <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <form id="edit-form"> | |
| <input type="hidden" id="edit-question-id"> | |
| <div class="mb-3"> | |
| <label for="edit-topic" class="form-label">Topic</label> | |
| <input type="text" class="form-control bg-secondary text-white" id="edit-topic" required> | |
| </div> | |
| <div class="mb-3"> | |
| <label for="edit-subject" class="form-label">Subject</label> | |
| <input type="text" class="form-control bg-secondary text-white" id="edit-subject" required> | |
| </div> | |
| </form> | |
| </div> | |
| <div class="modal-footer"> | |
| <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> | |
| <button type="button" class="btn btn-primary" id="save-changes-btn">Save changes</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Manual Classification Setup Modal --> | |
| <div class="modal fade" id="manualClassificationModal" tabindex="-1"> | |
| <div class="modal-dialog modal-lg"> | |
| <div class="modal-content bg-dark text-white"> | |
| <div class="modal-header border-secondary"> | |
| <h5 class="modal-title"><i class="bi bi-list-check me-2"></i>Quick Classification</h5> | |
| <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <div class="alert alert-info border-0 mb-3"> | |
| <i class="bi bi-lightning-fill me-2"></i> | |
| <span id="visible-count-info">Classify filtered questions with AI-powered suggestions.</span> | |
| </div> | |
| <!-- Quick Range Toggles --> | |
| <div class="mb-3"> | |
| <label class="form-label">Quick Select</label> | |
| <div class="d-flex flex-wrap gap-2"> | |
| <button type="button" class="btn btn-outline-primary range-toggle active" data-range="all"> | |
| <i class="bi bi-collection me-1"></i>All Visible | |
| </button> | |
| <button type="button" class="btn btn-outline-warning range-toggle" data-range="unclassified"> | |
| <i class="bi bi-question-circle me-1"></i>Unclassified | |
| </button> | |
| <button type="button" class="btn btn-outline-info range-toggle" data-range="custom"> | |
| <i class="bi bi-sliders me-1"></i>Custom Range | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Custom Range Slider UI --> | |
| <div id="custom-range-container" class="d-none"> | |
| <div class="card bg-secondary mb-3"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <label class="form-label mb-0">Select Question Ranges</label> | |
| <button type="button" class="btn btn-sm btn-success" onclick="addRangeSlider()"> | |
| <i class="bi bi-plus-lg me-1"></i>Add Range | |
| </button> | |
| </div> | |
| <div id="range-sliders-container"> | |
| <!-- Range sliders will be added here --> | |
| </div> | |
| <div class="mt-3 p-2 bg-dark rounded"> | |
| <small class="text-muted">Selected: </small> | |
| <span id="selected-ranges-display" class="text-info fw-bold">None</span> | |
| <small class="text-muted ms-2">(<span id="selected-count">0</span> questions)</small> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Preview Section --> | |
| <div class="row" id="range-preview-section"> | |
| <div class="col-6"> | |
| <div class="card bg-secondary"> | |
| <div class="card-header py-1 text-center"> | |
| <small class="text-success"><i class="bi bi-arrow-right-circle me-1"></i>Start</small> | |
| </div> | |
| <div class="card-body p-2"> | |
| <div id="preview-start-text" class="small" style="max-height: 60px; overflow: hidden;"></div> | |
| <div class="mt-1"><span class="badge bg-primary" id="preview-start-num">#1</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-6"> | |
| <div class="card bg-secondary"> | |
| <div class="card-header py-1 text-center"> | |
| <small class="text-danger"><i class="bi bi-arrow-left-circle me-1"></i>End</small> | |
| </div> | |
| <div class="card-body p-2"> | |
| <div id="preview-end-text" class="small" style="max-height: 60px; overflow: hidden;"></div> | |
| <div class="mt-1"><span class="badge bg-primary" id="preview-end-num">#1</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="modal-footer border-secondary"> | |
| <button type="button" class="btn btn-lg btn-primary w-100" onclick="startManualClassification()"> | |
| <i class="bi bi-play-fill me-2"></i>Start Classification | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Topic Selection Wizard Modal --> | |
| <div class="modal fade" id="topicSelectionModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false"> | |
| <div class="modal-dialog modal-lg modal-dialog-centered"> | |
| <div class="modal-content bg-dark text-white border-secondary"> | |
| <div class="modal-header border-secondary py-2"> | |
| <div class="d-flex align-items-center gap-3"> | |
| <span class="badge bg-primary fs-6" id="topic_q_num">#1</span> | |
| <div class="progress flex-grow-1" style="width: 150px; height: 6px;"> | |
| <div class="progress-bar" id="topic_progress_bar" role="progressbar" style="width: 0%"></div> | |
| </div> | |
| <small id="topic_progress" class="text-muted">1/10</small> | |
| </div> | |
| <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button> | |
| </div> | |
| <div class="modal-body"> | |
| <!-- Question Display --> | |
| <div class="mb-3 p-3 bg-black bg-opacity-25 rounded border border-secondary" style="max-height: 180px; overflow-y: auto;"> | |
| <div id="topic_q_text" class="text-white"></div> | |
| </div> | |
| <!-- Subject Selection (Auto-detected, editable) --> | |
| <div class="mb-3"> | |
| <label class="form-label small text-muted mb-2">Subject (click to change)</label> | |
| <div id="subject_pills" class="d-flex flex-wrap gap-2"> | |
| <button type="button" class="btn btn-outline-success subject-pill" data-subject="Biology"> | |
| <i class="bi bi-flower1 me-1"></i>Biology | |
| </button> | |
| <button type="button" class="btn btn-outline-warning subject-pill" data-subject="Chemistry"> | |
| <i class="bi bi-droplet me-1"></i>Chemistry | |
| </button> | |
| <button type="button" class="btn btn-outline-info subject-pill" data-subject="Physics"> | |
| <i class="bi bi-lightning me-1"></i>Physics | |
| </button> | |
| <button type="button" class="btn btn-outline-danger subject-pill" data-subject="Mathematics"> | |
| <i class="bi bi-calculator me-1"></i>Mathematics | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Chapter/Topic Input --> | |
| <div class="mb-2"> | |
| <label class="form-label small text-muted mb-2">Chapter / Topic</label> | |
| <div class="input-group input-group-lg"> | |
| <input type="text" id="topic_input" class="form-control bg-dark text-white border-secondary" placeholder="Start typing or use AI..."> | |
| <button class="btn btn-info" type="button" id="btn_get_suggestion" onclick="getAiSuggestion()"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status"></span> | |
| <i class="bi bi-stars"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Suggestions --> | |
| <div id="ai_suggestion_container" class="d-none"> | |
| <div id="ai_suggestion_chips" class="d-flex flex-wrap gap-2"></div> | |
| </div> | |
| <div id="ai_error_msg" class="alert alert-danger d-none mt-2 py-2"></div> | |
| </div> | |
| <div class="modal-footer border-secondary py-2"> | |
| <button type="button" class="btn btn-outline-secondary btn-lg px-4" onclick="prevTopicQuestion()"> | |
| <i class="bi bi-chevron-left"></i> | |
| </button> | |
| <button type="button" class="btn btn-primary btn-lg px-5" onclick="nextTopicQuestion()"> | |
| Next <i class="bi bi-chevron-right ms-1"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| // --- Manual Classification Logic (global functions) --- | |
| let manualQuestionsList = []; | |
| let currentManualIndex = 0; | |
| let manualSubject = "Biology"; // Will be auto-detected | |
| let suggestionCache = new Map(); | |
| let selectedRangeMode = 'all'; // 'all', 'unclassified', 'custom' | |
| let customRanges = []; // Array of {start, end, subject} for custom slider ranges | |
| let questionSubjectMap = new Map(); // Maps question index to pre-selected subject | |
| // Get question text preview by visible index | |
| function getQuestionPreview(visibleIndex) { | |
| const visibleRows = getVisibleRows(); | |
| if (visibleIndex >= 0 && visibleIndex < visibleRows.length) { | |
| const row = visibleRows[visibleIndex]; | |
| return row.querySelector('.question-text-cell').textContent.substring(0, 80) + '...'; | |
| } | |
| return ''; | |
| } | |
| // Get question number by visible index | |
| function getQuestionNumber(visibleIndex) { | |
| const visibleRows = getVisibleRows(); | |
| if (visibleIndex >= 0 && visibleIndex < visibleRows.length) { | |
| return visibleRows[visibleIndex].dataset.index; | |
| } | |
| return visibleIndex + 1; | |
| } | |
| // Add a new range slider | |
| function addRangeSlider() { | |
| const container = document.getElementById('range-sliders-container'); | |
| const visibleRows = getVisibleRows(); | |
| const maxVal = visibleRows.length; | |
| if (maxVal === 0) { | |
| alert("No visible questions to select from."); | |
| return; | |
| } | |
| const sliderId = Date.now(); | |
| const sliderHtml = ` | |
| <div class="range-slider-row" id="slider-row-${sliderId}" data-slider-id="${sliderId}"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <span class="badge bg-success" id="start-badge-${sliderId}">Q1</span> | |
| <i class="bi bi-arrow-right text-muted"></i> | |
| <span class="badge bg-danger" id="end-badge-${sliderId}">Q${maxVal}</span> | |
| </div> | |
| <div class="d-flex align-items-center gap-2"> | |
| <select class="form-select form-select-sm bg-dark text-white border-secondary" id="subject-${sliderId}" style="width: auto;"> | |
| <option value="auto">🤖 Auto-detect</option> | |
| <option value="Biology">🌱 Biology</option> | |
| <option value="Chemistry">🧪 Chemistry</option> | |
| <option value="Physics">⚡ Physics</option> | |
| <option value="Mathematics">📐 Mathematics</option> | |
| </select> | |
| <button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRangeSlider(${sliderId})"> | |
| <i class="bi bi-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="dual-range-container"> | |
| <div class="dual-range-track"></div> | |
| <div class="dual-range-highlight" id="highlight-${sliderId}"></div> | |
| <input type="range" min="1" max="${maxVal}" value="1" class="range-start" id="start-${sliderId}" | |
| oninput="updateRangeSlider(${sliderId})"> | |
| <input type="range" min="1" max="${maxVal}" value="${maxVal}" class="range-end" id="end-${sliderId}" | |
| oninput="updateRangeSlider(${sliderId})"> | |
| </div> | |
| </div> | |
| `; | |
| container.insertAdjacentHTML('beforeend', sliderHtml); | |
| customRanges.push({ id: sliderId, start: 1, end: maxVal, subject: 'auto' }); | |
| // Add subject change listener | |
| document.getElementById(`subject-${sliderId}`).addEventListener('change', (e) => { | |
| const rangeObj = customRanges.find(r => r.id === sliderId); | |
| if (rangeObj) rangeObj.subject = e.target.value; | |
| }); | |
| updateRangeSlider(sliderId); | |
| updateSelectedRangesDisplay(); | |
| updateRangePreview(); | |
| } | |
| // Update range slider visuals and values | |
| function updateRangeSlider(sliderId) { | |
| const startInput = document.getElementById(`start-${sliderId}`); | |
| const endInput = document.getElementById(`end-${sliderId}`); | |
| const highlight = document.getElementById(`highlight-${sliderId}`); | |
| const startBadge = document.getElementById(`start-badge-${sliderId}`); | |
| const endBadge = document.getElementById(`end-badge-${sliderId}`); | |
| let startVal = parseInt(startInput.value); | |
| let endVal = parseInt(endInput.value); | |
| // Ensure start <= end | |
| if (startVal > endVal) { | |
| if (startInput === document.activeElement) { | |
| endInput.value = startVal; | |
| endVal = startVal; | |
| } else { | |
| startInput.value = endVal; | |
| startVal = endVal; | |
| } | |
| } | |
| // Update highlight bar | |
| const max = parseInt(startInput.max); | |
| const leftPercent = ((startVal - 1) / (max - 1)) * 100; | |
| const rightPercent = ((endVal - 1) / (max - 1)) * 100; | |
| highlight.style.left = leftPercent + '%'; | |
| highlight.style.width = (rightPercent - leftPercent) + '%'; | |
| // Update badges with actual question numbers | |
| startBadge.textContent = `#${getQuestionNumber(startVal - 1)}`; | |
| endBadge.textContent = `#${getQuestionNumber(endVal - 1)}`; | |
| // Update stored range | |
| const rangeObj = customRanges.find(r => r.id === sliderId); | |
| if (rangeObj) { | |
| rangeObj.start = startVal; | |
| rangeObj.end = endVal; | |
| } | |
| updateSelectedRangesDisplay(); | |
| updateRangePreview(); | |
| } | |
| // Remove a range slider | |
| function removeRangeSlider(sliderId) { | |
| document.getElementById(`slider-row-${sliderId}`)?.remove(); | |
| customRanges = customRanges.filter(r => r.id !== sliderId); | |
| updateSelectedRangesDisplay(); | |
| updateRangePreview(); | |
| } | |
| // Update the display of selected ranges | |
| function updateSelectedRangesDisplay() { | |
| const display = document.getElementById('selected-ranges-display'); | |
| const countSpan = document.getElementById('selected-count'); | |
| if (customRanges.length === 0) { | |
| display.textContent = 'None'; | |
| countSpan.textContent = '0'; | |
| return; | |
| } | |
| const rangeStrs = customRanges.map(r => { | |
| const startNum = getQuestionNumber(r.start - 1); | |
| const endNum = getQuestionNumber(r.end - 1); | |
| return r.start === r.end ? `#${startNum}` : `#${startNum}-#${endNum}`; | |
| }); | |
| display.textContent = rangeStrs.join(', '); | |
| // Count unique questions | |
| const indices = new Set(); | |
| customRanges.forEach(r => { | |
| for (let i = r.start; i <= r.end; i++) indices.add(i); | |
| }); | |
| countSpan.textContent = indices.size; | |
| } | |
| // Update preview text | |
| function updateRangePreview() { | |
| if (customRanges.length === 0) return; | |
| // Find first and last question across all ranges | |
| let minQ = Infinity, maxQ = -Infinity; | |
| customRanges.forEach(r => { | |
| if (r.start < minQ) minQ = r.start; | |
| if (r.end > maxQ) maxQ = r.end; | |
| }); | |
| const startIdx = minQ - 1; | |
| const endIdx = maxQ - 1; | |
| document.getElementById('preview-start-text').textContent = getQuestionPreview(startIdx); | |
| document.getElementById('preview-end-text').textContent = getQuestionPreview(endIdx); | |
| document.getElementById('preview-start-num').textContent = `#${getQuestionNumber(startIdx)}`; | |
| document.getElementById('preview-end-num').textContent = `#${getQuestionNumber(endIdx)}`; | |
| } | |
| // Setup range toggle handlers | |
| function setupRangeToggles() { | |
| document.querySelectorAll('.range-toggle').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.range-toggle').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| selectedRangeMode = btn.dataset.range; | |
| // Show/hide custom range container | |
| const customContainer = document.getElementById('custom-range-container'); | |
| if (selectedRangeMode === 'custom') { | |
| customContainer.classList.remove('d-none'); | |
| // Add initial slider if none exist | |
| if (customRanges.length === 0) { | |
| addRangeSlider(); | |
| } | |
| } else { | |
| customContainer.classList.add('d-none'); | |
| } | |
| }); | |
| }); | |
| } | |
| // Subject detection keywords | |
| const SUBJECT_KEYWORDS = { | |
| 'Biology': ['cell', 'dna', 'rna', 'protein', 'enzyme', 'mitosis', 'meiosis', 'photosynthesis', 'respiration', 'chromosome', 'gene', 'mutation', 'evolution', 'species', 'organism', 'tissue', 'organ', 'plant', 'animal', 'bacteria', 'virus', 'ecology', 'ecosystem', 'biodiversity', 'reproduction', 'hormone', 'neuron', 'blood', 'heart', 'kidney', 'liver', 'digestion', 'excretion', 'inheritance', 'mendel', 'allele', 'genotype', 'phenotype', 'biomolecule', 'lipid', 'carbohydrate', 'amino acid'], | |
| 'Chemistry': ['atom', 'molecule', 'electron', 'proton', 'neutron', 'orbital', 'bond', 'ionic', 'covalent', 'reaction', 'oxidation', 'reduction', 'acid', 'base', 'ph', 'salt', 'equilibrium', 'mole', 'molarity', 'concentration', 'solution', 'organic', 'inorganic', 'alkane', 'alkene', 'alkyne', 'alcohol', 'aldehyde', 'ketone', 'ester', 'ether', 'amine', 'polymer', 'thermodynamics', 'enthalpy', 'entropy', 'electrochemistry', 'electrolysis', 'catalyst', 'kinetics', 'periodic table', 'element', 'compound'], | |
| 'Physics': ['force', 'velocity', 'acceleration', 'momentum', 'energy', 'work', 'power', 'newton', 'gravity', 'friction', 'motion', 'wave', 'frequency', 'wavelength', 'amplitude', 'sound', 'light', 'optics', 'lens', 'mirror', 'reflection', 'refraction', 'diffraction', 'interference', 'electric', 'current', 'voltage', 'resistance', 'capacitor', 'inductor', 'magnetic', 'electromagnetic', 'quantum', 'photon', 'nuclear', 'radioactive', 'thermodynamics', 'heat', 'temperature', 'pressure'], | |
| 'Mathematics': ['equation', 'function', 'derivative', 'integral', 'limit', 'matrix', 'determinant', 'vector', 'probability', 'statistics', 'mean', 'median', 'variance', 'trigonometry', 'sine', 'cosine', 'tangent', 'logarithm', 'exponential', 'polynomial', 'quadratic', 'linear', 'geometry', 'circle', 'triangle', 'parabola', 'ellipse', 'hyperbola', 'calculus', 'differentiation', 'integration', 'sequence', 'series', 'permutation', 'combination', 'set', 'relation', 'complex number'] | |
| }; | |
| // All NCERT chapters by subject for autocomplete | |
| const ALL_CHAPTERS = { | |
| 'Biology': [ | |
| 'The Living World', 'Biological Classification', 'Plant Kingdom', 'Animal Kingdom', | |
| 'Morphology of Flowering Plants', 'Anatomy of Flowering Plants', 'Structural Organisation in Animals', | |
| 'Cell: The Unit of Life', 'Biomolecules', 'Cell Cycle and Cell Division', | |
| 'Photosynthesis in Higher Plants', 'Respiration in Plants', 'Plant Growth and Development', | |
| 'Breathing and Exchange of Gases', 'Body Fluids and Circulation', 'Excretory Products and their Elimination', | |
| 'Locomotion and Movement', 'Neural Control and Coordination', 'Chemical Coordination and Integration', | |
| 'Sexual Reproduction in Flowering Plants', 'Human Reproduction', 'Reproductive Health', | |
| 'Principles of Inheritance and Variation', 'Molecular Basis of Inheritance', 'Evolution', | |
| 'Health and Disease', 'Improvement in Food Production', 'Microbes in Human Welfare', | |
| 'Biotechnology - Principles and Processes', 'Biotechnology and Its Applications', | |
| 'Organisms and Populations', 'Ecosystem', 'Biodiversity and Its Conservation' | |
| ], | |
| 'Chemistry': [ | |
| 'Some Basic Concepts of Chemistry', 'Structure of Atom', 'Classification of Elements and Periodicity in Properties', | |
| 'Chemical Bonding and Molecular Structure', 'States of Matter: Gases and Liquids', 'Thermodynamics', | |
| 'Equilibrium', 'Redox Reactions', 'Hydrogen', 'The s-Block Elements', 'The p-Block Elements (Group 13 and 14)', | |
| 'Organic Chemistry – Some Basic Principles and Techniques (GOC)', 'Hydrocarbons', 'Environmental Chemistry', | |
| 'The Solid State', 'Solutions', 'Electrochemistry', 'Chemical Kinetics', 'Surface Chemistry', | |
| 'General Principles and Processes of Isolation of Elements (Metallurgy)', 'The p-Block Elements (Group 15 to 18)', | |
| 'The d- and f- Block Elements', 'Coordination Compounds', 'Haloalkanes and Haloarenes', | |
| 'Alcohols, Phenols and Ethers', 'Aldehydes, Ketones and Carboxylic Acids', 'Amines', | |
| 'Biomolecules', 'Polymers', 'Chemistry in Everyday Life' | |
| ], | |
| 'Physics': [ | |
| 'Units and Measurements', 'Motion in a Straight Line', 'Motion in a Plane', 'Laws of Motion', | |
| 'Work, Energy and Power', 'System of Particles and Rotational Motion', 'Gravitation', | |
| 'Mechanical Properties of Solids', 'Mechanical Properties of Fluids', 'Thermal Properties of Matter', | |
| 'Thermodynamics', 'Kinetic Theory', 'Oscillations', 'Waves', | |
| 'Electric Charges and Fields', 'Electrostatic Potential and Capacitance', 'Current Electricity', | |
| 'Moving Charges and Magnetism', 'Magnetism and Matter', 'Electromagnetic Induction', | |
| 'Alternating Current', 'Electromagnetic Waves', 'Ray Optics and Optical Instruments', | |
| 'Wave Optics', 'Dual Nature of Radiation and Matter', 'Atoms', 'Nuclei', | |
| 'Semiconductor Electronics: Materials, Devices and Simple Circuits', 'Communication Systems' | |
| ], | |
| 'Mathematics': [ | |
| 'Sets', 'Relations and Functions', 'Trigonometric Functions', 'Principle of Mathematical Induction', | |
| 'Complex Numbers and Quadratic Equations', 'Linear Inequalities', 'Permutations and Combinations', | |
| 'Binomial Theorem', 'Sequences and Series', 'Straight Lines', 'Conic Sections', | |
| 'Introduction to Three Dimensional Geometry', 'Limits and Derivatives', 'Mathematical Reasoning', | |
| 'Statistics', 'Probability', 'Inverse Trigonometric Functions', 'Matrices', 'Determinants', | |
| 'Continuity and Differentiability', 'Application of Derivatives', 'Integrals', | |
| 'Application of Integrals', 'Differential Equations', 'Vector Algebra', | |
| 'Three Dimensional Geometry', 'Linear Programming' | |
| ] | |
| }; | |
| function detectSubject(text) { | |
| const lowerText = text.toLowerCase(); | |
| const scores = {}; | |
| for (const [subject, keywords] of Object.entries(SUBJECT_KEYWORDS)) { | |
| scores[subject] = 0; | |
| for (const keyword of keywords) { | |
| if (lowerText.includes(keyword)) { | |
| scores[subject]++; | |
| } | |
| } | |
| } | |
| // Sort by score and return top subjects | |
| const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]); | |
| const topSubjects = sorted.filter(([_, score]) => score > 0).slice(0, 2).map(([subj, _]) => subj); | |
| return topSubjects.length > 0 ? topSubjects : ['Biology']; // Default to Biology | |
| } | |
| function setActiveSubject(subject) { | |
| manualSubject = subject; | |
| document.querySelectorAll('.subject-pill').forEach(pill => { | |
| if (pill.dataset.subject === subject) { | |
| pill.classList.remove('btn-outline-success', 'btn-outline-warning', 'btn-outline-info', 'btn-outline-danger'); | |
| pill.classList.add('btn-success', 'btn-warning', 'btn-info', 'btn-danger'); | |
| // Keep the appropriate color | |
| if (subject === 'Biology') { pill.className = 'btn btn-success subject-pill'; } | |
| else if (subject === 'Chemistry') { pill.className = 'btn btn-warning subject-pill'; } | |
| else if (subject === 'Physics') { pill.className = 'btn btn-info subject-pill'; } | |
| else if (subject === 'Mathematics') { pill.className = 'btn btn-danger subject-pill'; } | |
| } else { | |
| // Reset to outline | |
| if (pill.dataset.subject === 'Biology') { pill.className = 'btn btn-outline-success subject-pill'; } | |
| else if (pill.dataset.subject === 'Chemistry') { pill.className = 'btn btn-outline-warning subject-pill'; } | |
| else if (pill.dataset.subject === 'Physics') { pill.className = 'btn btn-outline-info subject-pill'; } | |
| else if (pill.dataset.subject === 'Mathematics') { pill.className = 'btn btn-outline-danger subject-pill'; } | |
| } | |
| }); | |
| } | |
| function getVisibleRows() { | |
| // Get only currently visible (filtered) rows | |
| return Array.from(document.querySelectorAll('#questions-table-body tr')).filter(row => row.style.display !== 'none'); | |
| } | |
| function updateTypingSuggestions(inputValue) { | |
| const container = document.getElementById('ai_suggestion_container'); | |
| const chipsContainer = document.getElementById('ai_suggestion_chips'); | |
| if (!inputValue || inputValue.length < 2) { | |
| return; | |
| } | |
| const chapters = ALL_CHAPTERS[manualSubject] || []; | |
| const searchLower = inputValue.toLowerCase(); | |
| // Filter chapters that match the input | |
| const matches = chapters.filter(ch => ch.toLowerCase().includes(searchLower)).slice(0, 6); | |
| if (matches.length > 0) { | |
| container.classList.remove('d-none'); | |
| chipsContainer.innerHTML = ''; | |
| matches.forEach(suggestion => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'btn btn-outline-secondary btn-sm rounded-pill'; | |
| chip.innerText = suggestion; | |
| chip.onclick = () => { | |
| document.getElementById('topic_input').value = suggestion; | |
| chipsContainer.innerHTML = ''; | |
| container.classList.add('d-none'); | |
| }; | |
| chipsContainer.appendChild(chip); | |
| }); | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', function () { | |
| const editModal = new bootstrap.Modal(document.getElementById('editModal')); | |
| const editQuestionId = document.getElementById('edit-question-id'); | |
| const editTopic = document.getElementById('edit-topic'); | |
| const editSubject = document.getElementById('edit-subject'); | |
| document.querySelectorAll('.edit-btn').forEach(button => { | |
| button.addEventListener('click', () => { | |
| editQuestionId.value = button.dataset.id; | |
| editTopic.value = button.dataset.topic; | |
| editSubject.value = button.dataset.subject; | |
| }); | |
| }); | |
| document.getElementById('save-changes-btn').addEventListener('click', async () => { | |
| const id = editQuestionId.value; | |
| const topic = editTopic.value; | |
| const subject = editSubject.value; | |
| try { | |
| const response = await fetch(`/neetprep/update_question/${id}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ topic: topic, subject: subject }) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| const row = document.querySelector(`button[data-id='${id}']`).closest('tr'); | |
| row.querySelector('.topic-cell').textContent = topic; | |
| row.querySelector('.subject-cell').textContent = subject; | |
| const editBtn = row.querySelector('.edit-btn'); | |
| editBtn.dataset.topic = topic; | |
| editBtn.dataset.subject = subject; | |
| editModal.hide(); | |
| } else { | |
| alert(`Error: ${result.error}`); | |
| } | |
| } catch (error) { | |
| alert(`Request failed: ${error}`); | |
| } | |
| }); | |
| // --- Filtering Logic --- | |
| const searchInput = document.getElementById('search-input'); | |
| const topicFilter = document.getElementById('topic-filter'); | |
| const tableBody = document.getElementById('questions-table-body'); | |
| const rows = tableBody.getElementsByTagName('tr'); | |
| function filterTable() { | |
| const searchText = searchInput.value.toLowerCase(); | |
| const selectedTopic = topicFilter.value; | |
| for (let i = 0; i < rows.length; i++) { | |
| const row = rows[i]; | |
| const topic = row.dataset.topic; | |
| const text = row.textContent || row.innerText; | |
| const topicMatch = (selectedTopic === 'all' || topic === selectedTopic); | |
| const searchMatch = (text.toLowerCase().indexOf(searchText) > -1); | |
| if (topicMatch && searchMatch) { | |
| row.style.display = ""; | |
| } else { | |
| row.style.display = "none"; | |
| } | |
| } | |
| } | |
| searchInput.addEventListener('keyup', filterTable); | |
| topicFilter.addEventListener('change', filterTable); | |
| // Apply filter on page load (default to Unclassified) | |
| filterTable(); | |
| // Update visible count when modal opens | |
| document.getElementById('manualClassificationModal').addEventListener('show.bs.modal', () => { | |
| const visibleCount = getVisibleRows().length; | |
| document.getElementById('visible-count-info').textContent = | |
| `${visibleCount} questions currently visible. Classification will apply to these.`; | |
| // Reset slider state when modal opens | |
| customRanges = []; | |
| document.getElementById('range-sliders-container').innerHTML = ''; | |
| document.getElementById('custom-range-container').classList.add('d-none'); | |
| document.querySelectorAll('.range-toggle').forEach(b => b.classList.toggle('active', b.dataset.range === 'all')); | |
| selectedRangeMode = 'all'; | |
| }); | |
| // Setup range toggles | |
| setupRangeToggles(); | |
| // Live autocomplete as user types in topic input | |
| document.getElementById('topic_input').addEventListener('input', (e) => { | |
| updateTypingSuggestions(e.target.value); | |
| }); | |
| }); | |
| function parseRange(rangeStr, maxVal) { | |
| const indices = new Set(); | |
| const parts = rangeStr.split(','); | |
| for (let part of parts) { | |
| part = part.trim(); | |
| if (!part) continue; | |
| if (part.includes('-')) { | |
| const [start, end] = part.split('-').map(Number); | |
| if (!isNaN(start) && !isNaN(end)) { | |
| for (let i = start; i <= end; i++) indices.add(i); | |
| } | |
| } else { | |
| const num = Number(part); | |
| if (!isNaN(num)) indices.add(num); | |
| } | |
| } | |
| return Array.from(indices).filter(i => i >= 1 && i <= maxVal).sort((a, b) => a - b); | |
| } | |
| function startManualClassification() { | |
| // Get only visible (filtered) rows | |
| const visibleRows = getVisibleRows(); | |
| const numQuestions = visibleRows.length; | |
| if (numQuestions === 0) { | |
| alert("No questions visible. Please adjust your filter."); | |
| return; | |
| } | |
| // Build list based on selected mode | |
| questionSubjectMap.clear(); | |
| if (selectedRangeMode === 'custom') { | |
| // Use custom slider ranges with their subject settings | |
| if (customRanges.length === 0) { | |
| alert("Please add at least one range."); | |
| return; | |
| } | |
| const visibleIndices = visibleRows.map((row, idx) => ({ idx, qIndex: parseInt(row.dataset.index) })); | |
| const selectedIndices = new Set(); | |
| customRanges.forEach(r => { | |
| for (let i = r.start; i <= r.end; i++) { | |
| if (i <= visibleIndices.length) { | |
| const qIndex = visibleIndices[i - 1].qIndex; | |
| selectedIndices.add(qIndex); | |
| // Store subject for this question (if not auto) | |
| if (r.subject && r.subject !== 'auto') { | |
| questionSubjectMap.set(qIndex, r.subject); | |
| } | |
| } | |
| } | |
| }); | |
| manualQuestionsList = Array.from(selectedIndices).sort((a, b) => a - b); | |
| } else if (selectedRangeMode === 'unclassified') { | |
| // Only unclassified from visible | |
| manualQuestionsList = visibleRows | |
| .filter(row => { | |
| const topic = row.querySelector('.topic-cell').textContent; | |
| return !topic || topic === 'Unclassified'; | |
| }) | |
| .map(row => parseInt(row.dataset.index)); | |
| } else { | |
| // All visible questions | |
| manualQuestionsList = visibleRows.map(row => parseInt(row.dataset.index)); | |
| } | |
| if (manualQuestionsList.length === 0) { | |
| alert("No questions match the selected criteria."); | |
| return; | |
| } | |
| currentManualIndex = 0; | |
| suggestionCache.clear(); | |
| const modal1 = bootstrap.Modal.getInstance(document.getElementById('manualClassificationModal')); | |
| modal1.hide(); | |
| const modal2 = new bootstrap.Modal(document.getElementById('topicSelectionModal')); | |
| modal2.show(); | |
| // Setup subject pill click handlers | |
| document.querySelectorAll('.subject-pill').forEach(pill => { | |
| pill.onclick = () => { | |
| setActiveSubject(pill.dataset.subject); | |
| // Clear suggestions and show loading when subject changes | |
| const container = document.getElementById('ai_suggestion_container'); | |
| const chipsContainer = document.getElementById('ai_suggestion_chips'); | |
| container.classList.remove('d-none'); | |
| chipsContainer.innerHTML = '<span class="text-muted"><i class="bi bi-hourglass-split me-1"></i>Loading...</span>'; | |
| document.getElementById('topic_input').value = ''; | |
| // Clear cache for current question and refetch | |
| const qIndex = manualQuestionsList[currentManualIndex]; | |
| const row = document.querySelector(`#questions-table-body tr[data-index="${qIndex}"]`); | |
| if (row) { | |
| suggestionCache.delete(row.dataset.id); | |
| getAiSuggestion(); | |
| } | |
| }; | |
| }); | |
| loadTopicQuestion(); | |
| // Prefetch suggestions for up to 30 questions in background | |
| prefetchSuggestions(0, 30); | |
| } | |
| function prefetchSuggestions(startIndex, count) { | |
| // Group questions by their pre-selected subject for batch processing | |
| const subjectBatches = new Map(); // subject -> [{qIndex, questionId}] | |
| const autoDetectQueue = []; // Questions without pre-selected subject | |
| for (let i = startIndex; i < startIndex + count && i < manualQuestionsList.length; i++) { | |
| const qIndex = manualQuestionsList[i]; | |
| const row = document.querySelector(`#questions-table-body tr[data-index="${qIndex}"]`); | |
| if (!row) continue; | |
| const questionId = row.dataset.id; | |
| if (suggestionCache.has(questionId)) continue; | |
| const preSelectedSubject = questionSubjectMap.get(qIndex); | |
| if (preSelectedSubject) { | |
| // Group by subject for batch processing | |
| if (!subjectBatches.has(preSelectedSubject)) { | |
| subjectBatches.set(preSelectedSubject, []); | |
| } | |
| subjectBatches.get(preSelectedSubject).push({ qIndex, questionId }); | |
| } else { | |
| // Auto-detect: process individually | |
| autoDetectQueue.push({ qIndex, questionId }); | |
| } | |
| } | |
| // Process batched requests (up to 8 per subject per batch) | |
| for (const [subject, questions] of subjectBatches) { | |
| for (let i = 0; i < questions.length; i += 8) { | |
| const batch = questions.slice(i, i + 8); | |
| const questionIds = batch.map(q => q.questionId); | |
| // Create a single promise for the batch | |
| const batchPromise = fetch('/neetprep/get_suggestions_batch', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ question_ids: questionIds, subject: subject }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if (data.success && data.results) { | |
| return data.results; // Map of questionId -> result | |
| } | |
| throw new Error(data.error || 'Batch request failed'); | |
| }) | |
| .catch(err => { | |
| // Return fallback for all questions in batch | |
| const fallback = {}; | |
| questionIds.forEach(id => { | |
| fallback[id] = { status: 'error', suggestions: ['Unclassified'], subject: subject, error: err.message }; | |
| }); | |
| return fallback; | |
| }); | |
| // Store promise for each question that resolves to its specific result | |
| batch.forEach(q => { | |
| suggestionCache.set(q.questionId, batchPromise.then(results => { | |
| const result = results[q.questionId]; | |
| if (result) { | |
| return { | |
| status: 'done', | |
| suggestions: result.suggestions || ['Unclassified'], | |
| subject: result.subject || subject, | |
| otherSubjects: result.other_possible_subjects || [] | |
| }; | |
| } | |
| return { status: 'done', suggestions: ['Unclassified'], subject: subject, otherSubjects: [] }; | |
| })); | |
| }); | |
| } | |
| } | |
| // Process auto-detect questions individually | |
| for (const q of autoDetectQueue) { | |
| const promise = fetch(`/neetprep/get_suggestions/${q.questionId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({}) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if (data.success) { | |
| return { | |
| status: 'done', | |
| suggestions: data.suggestions || ['Unclassified'], | |
| subject: data.subject, | |
| otherSubjects: data.other_possible_subjects || [] | |
| }; | |
| } | |
| return { status: 'done', suggestions: ['Unclassified'], subject: 'Biology' }; | |
| }) | |
| .catch(err => ({ status: 'error', suggestions: ['Unclassified'], subject: 'Biology', error: err.message })); | |
| suggestionCache.set(q.questionId, promise); | |
| } | |
| } | |
| function loadTopicQuestion() { | |
| const qIndex = manualQuestionsList[currentManualIndex]; | |
| const row = document.querySelector(`#questions-table-body tr[data-index="${qIndex}"]`); | |
| if (!row) return; | |
| const questionId = row.dataset.id; | |
| const questionHtml = row.dataset.html || row.querySelector('.question-text-cell').textContent; | |
| const questionPlainText = row.querySelector('.question-text-cell').textContent; | |
| const currentTopic = row.querySelector('.topic-cell').textContent; | |
| const currentSubjectFromRow = row.querySelector('.subject-cell').textContent; | |
| // Check if subject was pre-selected in slider, otherwise auto-detect | |
| const preSelectedSubject = questionSubjectMap.get(qIndex); | |
| if (preSelectedSubject) { | |
| manualSubject = preSelectedSubject; | |
| } else if (currentSubjectFromRow && currentSubjectFromRow !== 'Unclassified' && ALL_CHAPTERS[currentSubjectFromRow]) { | |
| manualSubject = currentSubjectFromRow; | |
| } else { | |
| const detectedSubjects = detectSubject(questionPlainText); | |
| manualSubject = detectedSubjects[0]; | |
| } | |
| setActiveSubject(manualSubject); | |
| // Update UI | |
| document.getElementById('topic_q_num').innerText = `#${qIndex}`; | |
| document.getElementById('topic_q_text').innerHTML = questionHtml; // Render HTML | |
| document.getElementById('topic_input').value = (currentTopic && currentTopic !== 'Unclassified') ? currentTopic : ''; | |
| document.getElementById('topic_progress').innerText = `${currentManualIndex + 1}/${manualQuestionsList.length}`; | |
| // Update progress bar | |
| const progressPercent = ((currentManualIndex + 1) / manualQuestionsList.length) * 100; | |
| document.getElementById('topic_progress_bar').style.width = `${progressPercent}%`; | |
| // Clear previous suggestions | |
| document.getElementById('ai_suggestion_container').classList.add('d-none'); | |
| document.getElementById('ai_suggestion_chips').innerHTML = ''; | |
| document.getElementById('ai_error_msg').classList.add('d-none'); | |
| // Reset button | |
| const btn = document.getElementById('btn_get_suggestion'); | |
| btn.disabled = false; | |
| btn.querySelector('.spinner-border').classList.add('d-none'); | |
| // Auto-get suggestions if topic is empty | |
| if (!document.getElementById('topic_input').value) { | |
| getAiSuggestion(); | |
| } | |
| } | |
| async function nextTopicQuestion() { | |
| // Save in background (don't await - non-blocking) | |
| saveCurrentTopic(); | |
| if (currentManualIndex < manualQuestionsList.length - 1) { | |
| currentManualIndex++; | |
| loadTopicQuestion(); | |
| // Prefetch more suggestions as we progress (sliding window of 30) | |
| prefetchSuggestions(currentManualIndex + 1, 30); | |
| } else { | |
| const modal2 = bootstrap.Modal.getInstance(document.getElementById('topicSelectionModal')); | |
| modal2.hide(); | |
| alert("Manual classification completed!"); | |
| // Refresh the filter to show updated results | |
| document.getElementById('topic-filter').dispatchEvent(new Event('change')); | |
| } | |
| } | |
| function prevTopicQuestion() { | |
| if (currentManualIndex > 0) { | |
| currentManualIndex--; | |
| loadTopicQuestion(); | |
| } | |
| } | |
| function saveCurrentTopic() { | |
| const qIndex = manualQuestionsList[currentManualIndex]; | |
| const row = document.querySelector(`#questions-table-body tr[data-index="${qIndex}"]`); | |
| if (!row) return; | |
| const questionId = row.dataset.id; | |
| const topic = document.getElementById('topic_input').value || 'Unclassified'; | |
| // Update UI immediately | |
| row.querySelector('.topic-cell').textContent = topic; | |
| row.querySelector('.subject-cell').textContent = manualSubject; | |
| row.dataset.topic = topic; // Update data attribute for filtering | |
| const editBtn = row.querySelector('.edit-btn'); | |
| if (editBtn) { | |
| editBtn.dataset.topic = topic; | |
| editBtn.dataset.subject = manualSubject; | |
| } | |
| // Save to backend in background (fire and forget) | |
| fetch(`/neetprep/update_question/${questionId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ topic: topic, subject: manualSubject }) | |
| }).catch(e => console.error("Failed to save topic", e)); | |
| } | |
| async function getAiSuggestion() { | |
| const qIndex = manualQuestionsList[currentManualIndex]; | |
| const row = document.querySelector(`#questions-table-body tr[data-index="${qIndex}"]`); | |
| if (!row) return; | |
| const questionId = row.dataset.id; | |
| const btn = document.getElementById('btn_get_suggestion'); | |
| const container = document.getElementById('ai_suggestion_container'); | |
| const chipsContainer = document.getElementById('ai_suggestion_chips'); | |
| const errorDiv = document.getElementById('ai_error_msg'); | |
| btn.disabled = true; | |
| btn.querySelector('.spinner-border').classList.remove('d-none'); | |
| errorDiv.classList.add('d-none'); | |
| try { | |
| let result; | |
| if (suggestionCache.has(questionId)) { | |
| result = await suggestionCache.get(questionId); | |
| } else { | |
| // Check if subject was pre-selected in slider | |
| const preSelectedSubject = questionSubjectMap.get(qIndex); | |
| const requestBody = preSelectedSubject | |
| ? { subject: preSelectedSubject } | |
| : {}; // No subject - AI will detect | |
| const promise = fetch(`/neetprep/get_suggestions/${questionId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(requestBody) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => ({ | |
| status: 'done', | |
| suggestions: data.suggestions || ['Unclassified'], | |
| subject: data.subject, | |
| otherSubjects: data.other_possible_subjects || [] | |
| })) | |
| .catch(err => ({ status: 'error', suggestions: ['Unclassified'], subject: 'Biology', error: err.message })); | |
| suggestionCache.set(questionId, promise); | |
| result = await promise; | |
| } | |
| // Update subject from AI response | |
| if (result.subject) { | |
| setActiveSubject(result.subject); | |
| } | |
| container.classList.remove('d-none'); | |
| chipsContainer.innerHTML = ''; | |
| // Add a label for AI suggestions | |
| const label = document.createElement('span'); | |
| label.className = 'badge bg-info me-2'; | |
| label.innerText = 'AI'; | |
| chipsContainer.appendChild(label); | |
| // Show other possible subjects if detected | |
| if (result.otherSubjects && result.otherSubjects.length > 0) { | |
| const otherLabel = document.createElement('span'); | |
| otherLabel.className = 'badge bg-secondary me-2'; | |
| otherLabel.innerText = `Also: ${result.otherSubjects.join(', ')}`; | |
| otherLabel.title = 'This question may also belong to these subjects'; | |
| chipsContainer.appendChild(otherLabel); | |
| } | |
| result.suggestions.forEach(suggestion => { | |
| const chip = document.createElement('button'); | |
| chip.className = 'btn btn-outline-info btn-sm rounded-pill'; | |
| chip.innerText = suggestion; | |
| chip.onclick = () => { | |
| document.getElementById('topic_input').value = suggestion; | |
| }; | |
| chipsContainer.appendChild(chip); | |
| }); | |
| // Auto-fill if empty | |
| const currentVal = document.getElementById('topic_input').value; | |
| if (!currentVal && result.suggestions.length > 0) { | |
| document.getElementById('topic_input').value = result.suggestions[0]; | |
| } | |
| } catch (e) { | |
| errorDiv.innerText = `Error: ${e.message}`; | |
| errorDiv.classList.remove('d-none'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.querySelector('.spinner-border').classList.add('d-none'); | |
| } | |
| } | |
| </script> | |
| {% endblock %} | |