Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}NeetPrep Incorrect Questions{% endblock %} | |
| {% block head %} | |
| <style> | |
| body, html { | |
| background-color: var(--bg-dark); | |
| } | |
| .page-header { | |
| background: linear-gradient(135deg, var(--bg-dark), var(--bg-card)); | |
| border-radius: 12px; | |
| padding: 24px; | |
| margin-bottom: 24px; | |
| border: 1px solid var(--bg-elevated); | |
| } | |
| .page-header h2 { | |
| margin: 0; | |
| font-weight: 600; | |
| color: #fff; | |
| } | |
| .section-card { | |
| background-color: var(--bg-card); | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| border: 1px solid var(--bg-elevated); | |
| } | |
| .section-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| margin-bottom: 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .section-title i { | |
| color: var(--border-muted); | |
| } | |
| /* Sync controls */ | |
| .sync-controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| align-items: center; | |
| } | |
| .sync-btn { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| /* Source filter pills */ | |
| .source-filter-group { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .source-filter-group .btn-check:checked + .btn { | |
| box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.5); | |
| } | |
| /* Topic table */ | |
| .topic-table-container { | |
| max-height: 350px; | |
| overflow-y: auto; | |
| border-radius: 8px; | |
| border: 1px solid var(--bg-elevated); | |
| } | |
| .topic-table { | |
| margin-bottom: 0; | |
| } | |
| .topic-table thead th { | |
| position: sticky; | |
| top: 0; | |
| background-color: var(--bg-dark); | |
| border-bottom: 2px solid var(--border-subtle); | |
| font-weight: 600; | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| text-transform: uppercase; | |
| letter-spacing: 0.3px; | |
| padding: 12px 10px; | |
| } | |
| .topic-table tbody td { | |
| padding: 10px; | |
| vertical-align: middle; | |
| border-color: var(--bg-elevated); | |
| } | |
| .topic-table tbody tr:hover { | |
| background-color: rgba(13, 110, 253, 0.1); | |
| } | |
| .topic-name { | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| /* Action buttons grid */ | |
| .action-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); | |
| gap: 12px; | |
| } | |
| .action-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| padding: 12px 16px; | |
| font-weight: 500; | |
| border-radius: 50px; | |
| transition: all var(--transition-fast); | |
| } | |
| .action-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| /* Subject filter */ | |
| .subject-select { | |
| max-width: 300px; | |
| background-color: var(--bg-elevated); | |
| border-color: var(--border-subtle); | |
| color: #fff; | |
| } | |
| .subject-select:focus { | |
| background-color: var(--bg-elevated); | |
| border-color: var(--accent-primary); | |
| color: #fff; | |
| box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25); | |
| } | |
| /* Status alerts */ | |
| #sync-status .alert { | |
| border-radius: 8px; | |
| border: none; | |
| } | |
| /* Badge styling */ | |
| .count-badge { | |
| min-width: 40px; | |
| display: inline-block; | |
| text-align: center; | |
| } | |
| /* Checkbox styling */ | |
| .form-check-input { | |
| width: 18px; | |
| height: 18px; | |
| cursor: pointer; | |
| } | |
| .form-check-input:checked { | |
| background-color: var(--accent-primary); | |
| border-color: var(--accent-primary); | |
| } | |
| /* Selection info bar */ | |
| .selection-info { | |
| background-color: var(--bg-elevated); | |
| border-radius: 8px; | |
| padding: 10px 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 12px; | |
| } | |
| .selection-count { | |
| font-weight: 500; | |
| color: var(--accent-primary); | |
| } | |
| /* Tag chips */ | |
| .tag-chip { | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| padding: 4px 12px; | |
| transition: all 0.15s ease; | |
| } | |
| .tag-chip.active { | |
| background-color: var(--accent-primary); | |
| border-color: var(--accent-primary); | |
| color: white; | |
| } | |
| .tag-chip:hover:not(.active) { | |
| background-color: rgba(13, 110, 253, 0.15); | |
| border-color: var(--accent-primary); | |
| } | |
| .tag-search-container input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="container-fluid mt-4" style="width: 90%; margin: auto;"> | |
| <!-- Page Header --> | |
| <div class="page-header"> | |
| <div class="d-flex justify-content-between align-items-center flex-wrap gap-3"> | |
| <h2><i class="bi bi-journal-x me-2"></i>Incorrect Question Manager</h2> | |
| <div class="d-flex gap-2"> | |
| {% if neetprep_enabled %} | |
| <a href="/neetprep/edit" class="btn btn-outline-light btn-sm"><i class="bi bi-pencil me-1"></i>Edit Synced</a> | |
| {% endif %} | |
| <a href="/classified/edit" class="btn btn-outline-light btn-sm"><i class="bi bi-pencil me-1"></i>Edit Classified</a> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <!-- Left Column: Controls --> | |
| <div class="col-lg-4"> | |
| {% if neetprep_enabled %} | |
| <!-- Sync Section --> | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-cloud-download"></i>NeetPrep Sync</div> | |
| <div class="sync-controls"> | |
| <button id="sync-btn" class="btn btn-primary sync-btn"> | |
| <i class="bi bi-arrow-repeat"></i>Sync Questions | |
| </button> | |
| <div class="form-check"> | |
| <input class="form-check-input" type="checkbox" id="force-sync-checkbox"> | |
| <label class="form-check-label text-muted" for="force-sync-checkbox">Force full re-sync</label> | |
| </div> | |
| </div> | |
| <hr class="my-3 border-secondary"> | |
| <div class="d-flex align-items-center gap-3"> | |
| <button id="classify-btn" class="btn btn-warning btn-sm"> | |
| <i class="bi bi-tags me-1"></i>Classify | |
| </button> | |
| <span class="text-muted small">{{ unclassified_count }} unclassified</span> | |
| </div> | |
| </div> | |
| {% else %} | |
| <div class="section-card"> | |
| <div class="alert alert-info mb-0" role="alert"> | |
| <i class="bi bi-info-circle me-2"></i> | |
| NeetPrep Sync is disabled. Enable it in <a href="{{ url_for('settings.settings') }}" class="alert-link">Settings</a>. | |
| </div> | |
| </div> | |
| {% endif %} | |
| <!-- Subject Filter --> | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-funnel"></i>Filter by Subject</div> | |
| <form id="subject-filter-form" method="get" action="{{ url_for('neetprep_bp.index') }}"> | |
| <select name="subject" id="subject" class="form-select subject-select" onchange="this.form.submit()"> | |
| {% for subject in available_subjects %} | |
| <option value="{{ subject }}" {% if subject == selected_subject %}selected{% endif %}>{{ subject }}</option> | |
| {% endfor %} | |
| </select> | |
| </form> | |
| </div> | |
| <!-- Question Source --> | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-collection"></i>Question Source</div> | |
| <div class="source-filter-group"> | |
| <input type="radio" class="btn-check" name="source_filter" id="source-all" value="all" checked> | |
| <label class="btn btn-outline-secondary btn-sm" for="source-all">All</label> | |
| {% if neetprep_enabled %} | |
| <input type="radio" class="btn-check" name="source_filter" id="source-neetprep" value="neetprep"> | |
| <label class="btn btn-outline-info btn-sm" for="source-neetprep">NeetPrep</label> | |
| {% endif %} | |
| <input type="radio" class="btn-check" name="source_filter" id="source-classified" value="classified"> | |
| <label class="btn btn-outline-success btn-sm" for="source-classified">Classified</label> | |
| </div> | |
| </div> | |
| <!-- Tag Filter --> | |
| {% if available_tags %} | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-tags"></i>Filter by Tags</div> | |
| <div class="tag-search-container mb-2"> | |
| <input type="text" class="form-control form-control-sm" id="tag-search" placeholder="Search tags..." style="background: var(--bg-elevated); border-color: var(--border-subtle); color: #fff;"> | |
| </div> | |
| <div class="tag-chips-container" id="tag-chips" style="max-height: 150px; overflow-y: auto;"> | |
| {% for tag in available_tags %} | |
| <button type="button" class="btn btn-outline-secondary btn-sm tag-chip me-1 mb-1" data-tag="{{ tag }}"> | |
| {{ tag }} | |
| </button> | |
| {% endfor %} | |
| </div> | |
| <div class="selected-tags mt-2" id="selected-tags-display" style="display: none;"> | |
| <small class="text-muted">Selected: </small> | |
| <span id="selected-tags-list"></span> | |
| <button type="button" class="btn btn-link btn-sm text-danger p-0 ms-2" onclick="clearAllTags()">Clear</button> | |
| </div> | |
| </div> | |
| {% endif %} | |
| <!-- Status Messages --> | |
| <div id="sync-status"></div> | |
| <div id="pdf-link-container"></div> | |
| </div> | |
| <!-- Right Column: Topics & Actions --> | |
| <div class="col-lg-8"> | |
| <form id="generate-pdf-form"> | |
| <!-- Quick Actions --> | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-lightning"></i>Quick Actions</div> | |
| <div class="action-grid"> | |
| {% if neetprep_enabled %} | |
| <button type="submit" name="pdf_type" value="all" class="btn btn-success action-btn"> | |
| <i class="bi bi-file-pdf"></i>Generate All PDF | |
| </button> | |
| {% endif %} | |
| <button type="submit" name="pdf_type" value="quiz" class="btn btn-primary action-btn"> | |
| <i class="bi bi-play-circle"></i>Start Quiz | |
| </button> | |
| <button type="submit" name="pdf_type" value="selected" class="btn btn-info action-btn" {% if not topics %}disabled{% endif %}> | |
| <i class="bi bi-check2-square"></i>Generate Selected | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Topic Selection --> | |
| <div class="section-card"> | |
| <div class="section-title"><i class="bi bi-list-check"></i>Select Topics</div> | |
| {% if topics %} | |
| <!-- Selection Info Bar --> | |
| <div class="selection-info"> | |
| <div> | |
| <input class="form-check-input me-2" type="checkbox" id="select-all-topics"> | |
| <label for="select-all-topics" class="form-check-label">Select All</label> | |
| </div> | |
| <span class="selection-count"><span id="selected-count">0</span> / {{ topics|length }} selected</span> | |
| </div> | |
| {% endif %} | |
| <div class="topic-table-container"> | |
| <table class="table table-dark table-hover topic-table"> | |
| <thead> | |
| <tr> | |
| <th style="width: 50px;">#</th> | |
| <th style="width: 50px;"></th> | |
| <th>Topic</th> | |
| {% if neetprep_enabled %}<th style="width: 100px;">NeetPrep</th>{% endif %} | |
| <th style="width: 100px;">My Qs</th> | |
| </tr> | |
| </thead> | |
| <tbody id="topic-list"> | |
| {% if topics %} | |
| {% for item in topics %} | |
| <tr> | |
| <td class="text-muted">{{ loop.index }}</td> | |
| <td><input class="form-check-input topic-checkbox" type="checkbox" value="{{ item.topic }}"></td> | |
| <td class="topic-name">{{ item.topic }}</td> | |
| {% if neetprep_enabled %}<td><span class="badge bg-secondary count-badge">{{ item.neetprep_count }}</span></td>{% endif %} | |
| <td><span class="badge bg-info count-badge">{{ item.my_questions_count }}</span></td> | |
| </tr> | |
| {% endfor %} | |
| {% else %} | |
| <tr> | |
| <td colspan="{% if neetprep_enabled %}5{% else %}4{% endif %}" class="text-center text-muted py-4"> | |
| <i class="bi bi-inbox fs-1 d-block mb-2"></i> | |
| {% if neetprep_enabled %} | |
| No topics found. Sync with NeetPrep to fetch questions. | |
| {% else %} | |
| No classified topics found. | |
| {% endif %} | |
| </td> | |
| </tr> | |
| {% endif %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| function loadSettings() { | |
| const settings = JSON.parse(localStorage.getItem('pdfGeneratorSettings')); | |
| if (settings) { | |
| return settings; | |
| } | |
| return { | |
| images_per_page: 4, | |
| orientation: 'portrait', | |
| grid_rows: 4, | |
| grid_cols: 1, | |
| practice_mode: 'none' | |
| }; | |
| } | |
| // Select all checkbox functionality | |
| const selectAllCheckbox = document.getElementById('select-all-topics'); | |
| const topicCheckboxes = document.querySelectorAll('.topic-checkbox'); | |
| const selectedCountSpan = document.getElementById('selected-count'); | |
| function updateSelectedCount() { | |
| const count = document.querySelectorAll('.topic-checkbox:checked').length; | |
| if (selectedCountSpan) selectedCountSpan.textContent = count; | |
| // Update select all checkbox state | |
| if (selectAllCheckbox) { | |
| selectAllCheckbox.checked = count === topicCheckboxes.length && topicCheckboxes.length > 0; | |
| selectAllCheckbox.indeterminate = count > 0 && count < topicCheckboxes.length; | |
| } | |
| } | |
| if (selectAllCheckbox) { | |
| selectAllCheckbox.addEventListener('change', () => { | |
| topicCheckboxes.forEach(cb => cb.checked = selectAllCheckbox.checked); | |
| updateSelectedCount(); | |
| }); | |
| } | |
| topicCheckboxes.forEach(cb => { | |
| cb.addEventListener('change', updateSelectedCount); | |
| }); | |
| // Tag filtering functionality | |
| const selectedTags = new Set(); | |
| const tagChips = document.querySelectorAll('.tag-chip'); | |
| const tagSearchInput = document.getElementById('tag-search'); | |
| const selectedTagsDisplay = document.getElementById('selected-tags-display'); | |
| const selectedTagsList = document.getElementById('selected-tags-list'); | |
| function updateTagsDisplay() { | |
| if (selectedTags.size > 0) { | |
| selectedTagsDisplay.style.display = 'block'; | |
| selectedTagsList.textContent = Array.from(selectedTags).join(', '); | |
| } else { | |
| selectedTagsDisplay.style.display = 'none'; | |
| } | |
| } | |
| tagChips.forEach(chip => { | |
| chip.addEventListener('click', () => { | |
| const tag = chip.dataset.tag; | |
| if (selectedTags.has(tag)) { | |
| selectedTags.delete(tag); | |
| chip.classList.remove('active'); | |
| } else { | |
| selectedTags.add(tag); | |
| chip.classList.add('active'); | |
| } | |
| updateTagsDisplay(); | |
| }); | |
| }); | |
| if (tagSearchInput) { | |
| tagSearchInput.addEventListener('input', (e) => { | |
| const searchTerm = e.target.value.toLowerCase(); | |
| tagChips.forEach(chip => { | |
| const tag = chip.dataset.tag.toLowerCase(); | |
| if (tag.includes(searchTerm)) { | |
| chip.style.display = ''; | |
| } else { | |
| chip.style.display = 'none'; | |
| } | |
| }); | |
| }); | |
| } | |
| function clearAllTags() { | |
| selectedTags.clear(); | |
| tagChips.forEach(chip => chip.classList.remove('active')); | |
| updateTagsDisplay(); | |
| } | |
| // Only attach event listeners if NeetPrep is enabled | |
| {% if neetprep_enabled %} | |
| document.getElementById('sync-btn').addEventListener('click', async () => { | |
| const syncStatus = document.getElementById('sync-status'); | |
| const forceSync = document.getElementById('force-sync-checkbox').checked; | |
| syncStatus.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split me-2"></i>Syncing... This may take a while.</div>'; | |
| try { | |
| const response = await fetch('/neetprep/sync', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ force: forceSync }) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| syncStatus.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle me-2"></i>${result.status}</div>`; | |
| setTimeout(() => window.location.reload(), 2000); | |
| } else { | |
| syncStatus.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Error: ${result.error || 'Unknown error'}</div>`; | |
| } | |
| } catch (error) { | |
| syncStatus.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Request failed: ${error}</div>`; | |
| } | |
| }); | |
| document.getElementById('classify-btn').addEventListener('click', async () => { | |
| const syncStatus = document.getElementById('sync-status'); | |
| syncStatus.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split me-2"></i>Classification started... This will take time due to rate limits.</div>'; | |
| try { | |
| const response = await fetch('/neetprep/classify', { method: 'POST' }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| syncStatus.innerHTML = `<div class="alert alert-success"><i class="bi bi-check-circle me-2"></i>${result.status}</div>`; | |
| setTimeout(() => window.location.reload(), 2000); | |
| } else { | |
| syncStatus.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Error: ${result.error || 'Unknown error'}</div>`; | |
| } | |
| } catch (error) { | |
| syncStatus.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Request failed: ${error}</div>`; | |
| } | |
| }); | |
| {% endif %} | |
| document.getElementById('generate-pdf-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const pdfType = e.submitter.value; | |
| const statusDiv = document.getElementById('sync-status'); | |
| const settings = loadSettings(); | |
| const sourceFilter = document.querySelector('input[name="source_filter"]:checked').value; | |
| if (pdfType === 'quiz') { | |
| const selectedTopics = Array.from(document.querySelectorAll('#topic-list .topic-checkbox:checked')).map(cb => cb.value); | |
| if (selectedTopics.length === 0) { | |
| statusDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-exclamation-circle me-2"></i>Please select at least one topic for the quiz.</div>'; | |
| return; | |
| } | |
| const form = document.createElement('form'); | |
| form.method = 'POST'; | |
| form.action = '/neetprep/generate'; | |
| const typeInput = document.createElement('input'); | |
| typeInput.type = 'hidden'; | |
| typeInput.name = 'type'; | |
| typeInput.value = 'quiz'; | |
| form.appendChild(typeInput); | |
| const topicsInput = document.createElement('input'); | |
| topicsInput.type = 'hidden'; | |
| topicsInput.name = 'topics'; | |
| topicsInput.value = JSON.stringify(selectedTopics); | |
| form.appendChild(topicsInput); | |
| const sourceInput = document.createElement('input'); | |
| sourceInput.type = 'hidden'; | |
| sourceInput.name = 'source'; | |
| sourceInput.value = sourceFilter; | |
| form.appendChild(sourceInput); | |
| // Include selected tags | |
| const tagsInput = document.createElement('input'); | |
| tagsInput.type = 'hidden'; | |
| tagsInput.name = 'tags'; | |
| tagsInput.value = JSON.stringify(Array.from(selectedTags)); | |
| form.appendChild(tagsInput); | |
| document.body.appendChild(form); | |
| form.submit(); | |
| return; | |
| } | |
| let payload = { | |
| type: pdfType, | |
| source: sourceFilter, | |
| tags: JSON.stringify(Array.from(selectedTags)), | |
| layout: { | |
| images_per_page: settings.images_per_page, | |
| orientation: settings.orientation, | |
| grid_rows: settings.grid_rows, | |
| grid_cols: settings.grid_cols, | |
| practice_mode: settings.practice_mode | |
| } | |
| }; | |
| if (pdfType === 'selected') { | |
| const selectedTopics = Array.from(document.querySelectorAll('#topic-list .topic-checkbox:checked')).map(cb => cb.value); | |
| if (selectedTopics.length === 0) { | |
| statusDiv.innerHTML = '<div class="alert alert-warning"><i class="bi bi-exclamation-circle me-2"></i>Please select at least one topic.</div>'; | |
| return; | |
| } | |
| payload.topics = selectedTopics; | |
| } | |
| statusDiv.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split me-2"></i>Generating PDF...</div>'; | |
| try { | |
| const response = await fetch('/neetprep/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const result = await response.json(); | |
| if (response.ok && result.success) { | |
| statusDiv.innerHTML = '<div class="alert alert-success"><i class="bi bi-check-circle me-2"></i>PDF generated!</div>'; | |
| if(result.pdf_url) { | |
| const pdfLinkContainer = document.getElementById('pdf-link-container'); | |
| pdfLinkContainer.innerHTML = `<a href="${result.pdf_url}" class="btn btn-outline-light" target="_blank"><i class="bi bi-file-pdf me-2"></i>View Generated PDF</a>`; | |
| window.open(result.pdf_url, '_blank'); | |
| } | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Error: ${result.error || 'PDF generation failed.'}</div>`; | |
| } | |
| } catch (error) { | |
| statusDiv.innerHTML = `<div class="alert alert-danger"><i class="bi bi-exclamation-triangle me-2"></i>Request failed: ${error}</div>`; | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |