Report-Generator / templates /neetprep.html
t
feat: major UX overhaul for PDF viewer, drive browser, and NeetPrep
693a1e9
{% 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 %}