Report-Generator / templates /question_entry_v2.html
Jaimodiji's picture
Upload folder using huggingface_hub
c001f24
Raw
History Blame
46.5 kB
{% extends "base.html" %}
{% block title %}Enter Question Details (V2){% endblock %}
{% block head %}
<style>
.keyboard-hint { font-size: 0.8em; color: #6c757d; margin-top: 2px; }
.status-buttons { display: flex; gap: 0.25rem; margin-top: 0.25rem; }
.status-btn { flex: 1; font-size: 0.75rem; padding: 0.25rem 0.5rem; border: 1px solid #495057; background: transparent; color: #fff; border-radius: 0.25rem; cursor: pointer; transition: all 0.2s; }
.status-btn.active { background: #0d6efd; border-color: #0d6efd; }
.status-btn:hover { background: #495057; }
.auto-extract-btn { min-width: 120px; }
/* Tom Select Dark Theme */
.ts-wrapper .ts-control, .ts-wrapper .ts-control input {
background: #212529;
color: #fff;
border-color: #6c757d;
}
.ts-dropdown {
background: #212529;
color: #fff;
border-color: #6c757d;
}
.ts-dropdown .option {
color: #fff;
}
.ts-dropdown .option:hover, .ts-dropdown .active {
background: #495057;
}
</style>
{% endblock %}
{% block content %}
<div class="container mt-3 mb-5">
<div class="card bg-dark text-white">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">Step 3: Enter Question Details</h1>
<div class="keyboard-hint">Shortcuts: Shift+Q for Next | Shift+C (Correct) | Shift+W (Wrong) | Shift+U (Unattempted)</div>
{% if classified_count is defined and total_questions is defined %}
<div class="d-flex align-items-center mt-1">
<span class="badge bg-info">
<i class="bi bi-tags me-1"></i>Classification:
<strong>{{ classified_count }}</strong>/{{ total_questions }} questions
</span>
</div>
{% endif %}
</div>
<div>
<a href="/cropv2/{{ session_id }}/0" class="btn btn-secondary"><i class="bi bi-arrow-left me-1"></i> Go Back to Cropping</a>
</div>
</div>
<div class="card-body">
<fieldset class="mb-4">
<legend class="h5">Upload Answer Key (Optional)</legend>
<div class="mb-3">
<label for="json-upload" class="form-label">Upload a JSON file to auto-fill answers.</label>
<input class="form-control" type="file" id="json-upload" accept=".json">
</div>
</fieldset>
<form id="questions-form">
{% if not nvidia_nim_available %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
NVIDIA NIM OCR feature is not available. To enable automatic question number extraction, please set the <code>NVIDIA_API_KEY</code> environment variable.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% else %}
<div class="mb-3">
<button id="auto-extract-all" class="btn btn-primary">
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span class="extract-text">Auto Extract All Question Numbers</span>
</button>
<button id="extract-classify-all" class="btn btn-info">
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span class="extract-text">Extract & Classify All</span>
</button>
</div>
{% endif %}
{% for image in images %}
<fieldset class="row mb-4 border-bottom pb-3" data-question-index="{{ loop.index0 }}" id="question-fieldset-{{ image.id }}">
<input type="hidden" name="image_id_{{ loop.index0 }}" value="{{ image.id }}">
<legend class="h5 col-12 mb-3">Question {{ loop.index }}
<button type="button" class="btn btn-sm btn-outline-danger float-end delete-question-btn" data-image-id="{{ image.id }}" data-index="{{ loop.index0 }}">
<i class="bi bi-trash"></i> Delete
</button>
</legend>
<div class="col-md-3 mb-3">
<img src="/image/processed/{{ session_id }}/{{ image.processed_filename }}" class="img-fluid rounded" alt="Cropped Question {{ loop.index }}">
</div>
<div class="col-md-9">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="question_number_{{ loop.index0 }}">Question Number *</label>
{% if nvidia_nim_available %}
<div class="input-group">
<input type="number" class="form-control" id="question_number_{{ loop.index0 }}" name="question_number_{{ loop.index0 }}" data-image-id="{{ image.id }}" value="{{ image.question_number or '' }}" required>
<button class="btn btn-outline-secondary auto-extract-btn" type="button" data-image-id="{{ image.id }}" data-index="{{ loop.index0 }}">
<span class="extract-spinner spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span class="extract-text">Auto Extract</span>
</button>
</div>
{% else %}
<input type="number" class="form-control" id="question_number_{{ loop.index0 }}" name="question_number_{{ loop.index0 }}" data-image-id="{{ image.id }}" value="{{ image.question_number or '' }}" required>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="marked_solution_{{ loop.index0 }}">Your Answer</label>
<input type="text" class="form-control" id="marked_solution_{{ loop.index0 }}" name="marked_solution_{{ loop.index0 }}" value="{{ image.marked_solution or '' }}">
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="actual_solution_{{ loop.index0 }}">Correct Answer</label>
<input type="text" class="form-control" id="actual_solution_{{ loop.index0 }}" name="actual_solution_{{ loop.index0 }}" value="{{ image.actual_solution or '' }}">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Status</label>
<select class="form-select d-none" id="status_{{ loop.index0 }}" name="status_{{ loop.index0 }}">
<option value="correct" {% if image.status and image.status.lower() == 'correct' %}selected{% endif %}>Correct</option>
<option value="wrong" {% if image.status and image.status.lower() == 'wrong' %}selected{% endif %}>Wrong</option>
<option value="unattempted" {% if image.status and image.status.lower() == 'unattempted' or not image.status %}selected{% endif %}>Unattempted</option>
</select>
<div class="status-buttons" data-index="{{ loop.index0 }}">
<button type="button" class="status-btn {% if image.status and image.status.lower() == 'correct' %}active{% endif %}" data-status="correct">✓ Correct</button>
<button type="button" class="status-btn {% if image.status and image.status.lower() == 'wrong' %}active{% endif %}" data-status="wrong">✗ Wrong</button>
<button type="button" class="status-btn {% if image.status and image.status.lower() == 'unattempted' or not image.status %}active{% endif %}" data-status="unattempted">? Unattempted</button>
</div>
</div>
<div class="row mt-2">
<div class="col-md-6">
<p class="mb-0"><strong>Subject:</strong> <span id="subject_{{ image.id }}">{{ image.subject or 'N/A' }}</span></p>
</div>
<div class="col-md-6">
<p class="mb-0"><strong>Chapter:</strong> <span id="chapter_{{ image.id }}">{{ image.chapter or 'N/A' }}</span></p>
</div>
</div>
</fieldset>
{% endfor %}
<fieldset>
<legend class="h4">Generate PDF</legend>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="pdf_subject">PDF Subject *</label>
<input type="text" id="pdf_subject" class="form-control" value="{{ session_data.subject or session_data.original_filename or '' }}" required>
</div>
<div class="col-md-6 mb-3" id="pdf-name-div">
<label class="form-label" for="pdf_name">PDF Name</label>
<input type="text" id="pdf_name" class="form-control" value="{{ session_data.original_filename or '' }}">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label" for="pdf_tags">Tags (comma-separated)</label>
<input type="text" id="pdf_tags" class="form-control" value="{{ session_data.tags or '' }}">
</div>
<div class="col-md-6 mb-3">
<label class="form-label" for="pdf_notes">Notes</label>
<textarea id="pdf_notes" class="form-control" rows="1">{{ session_data.notes or '' }}</textarea>
</div>
</div>
<hr>
<legend class="h5">Layout Options</legend>
<div class="row align-items-end">
<div class="col-md-3 mb-3">
<label class="form-label" for="practice_mode">Practice Mode</label>
<select id="practice_mode" class="form-select">
<option value="none" selected>None</option>
<option value="portrait_2">Portrait 2</option>
<option value="portrait_3">Portrait 3</option>
<option value="landscape_2">Landscape 2</option>
<option value="portrait_2_spacious">Portrait 2 - Spacious</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="orientation">Orientation</label>
<select id="orientation" class="form-select">
<option value="portrait" selected>Portrait</option>
<option value="landscape">Landscape</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="filter_type">Filter Questions</label>
<select id="filter_type" class="form-select">
<option value="all">All</option>
<option value="wrong">Wrong Only</option>
<option value="unattempted">Unattempted Only</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="font_size_scale">Font Size Scale: <span id="font_size_val">1.0</span>x</label>
<input type="range" class="form-range" id="font_size_scale" min="0.5" max="2.0" step="0.1" value="1.0" oninput="document.getElementById('font_size_val').innerText = this.value">
</div>
</div>
<div id="standard-layout-options">
<div class="row">
<div class="col-md-3 mb-3">
<label class="form-label" for="images_per_page">Images per Page</label>
<input type="number" id="images_per_page" class="form-control" value="4" min="1" max="20">
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="grid_rows">Grid Rows</label>
<input type="number" id="grid_rows" class="form-control" value="4" min="1">
</div>
<div class="col-md-3 mb-3">
<label class="form-label" for="grid_cols">Grid Cols</label>
<input type="number" id="grid_cols" class="form-control" value="1" min="1">
</div>
</div>
</div>
</fieldset>
<div class="d-flex gap-2 mt-3">
<button type="button" id="preview-btn" class="btn btn-secondary w-100">See Preview</button>
<button type="submit" class="btn btn-primary w-100">Generate PDF</button>
</div>
</form>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">PDF Preview</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img id="preview-img" src="" class="img-fluid" alt="PDF Preview">
</div>
</div>
</div>
</div>
<div class="accordion mt-4" id="misc-accordion">
<div class="accordion-item bg-dark">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button bg-secondary text-white collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseMisc" aria-expanded="false" aria-controls="collapseMisc">
Add Miscellaneous Questions (Temporary)
</button>
</h2>
<div id="collapseMisc" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#misc-accordion">
<div class="accordion-body">
<form id="misc-question-form">
<div class="row">
<div class="col-md-6 mb-3"><label class="form-label">Question Number</label><input type="text" id="misc_question_number" class="form-control"></div>
<div class="col-md-6 mb-3"><label class="form-label">Subject</label><input type="text" id="misc_subject" class="form-control"></div>
<div class="col-md-6 mb-3"><label class="form-label">Your Answer</label><input type="text" id="misc_marked_solution" class="form-control"></div>
<div class="col-md-6 mb-3"><label class="form-label">Correct Answer</label><input type="text" id="misc_actual_solution" class="form-control"></div>
<div class="col-md-6 mb-3"><label class="form-label">Status</label><select id="misc_status" class="form-select"><option value="correct">Correct</option><option value="wrong">Wrong</option><option value="unattempted" selected>Unattempted</option></select></div>
<div class="col-md-6 mb-3"><label class="form-label">Image (Optional)</label><input type="file" id="misc_image" class="form-control" accept="image/*"></div>
</div>
<button type="button" id="add-misc-question" class="btn btn-info">Add Miscellaneous Question</button>
</form>
<hr class="my-4">
<h4 class="h5">Added Miscellaneous Questions:</h4>
<div id="misc-questions-list"></div>
</div>
</div>
</div>
</div>
<div id="status" class="mt-3"></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const numImages = {{ images|length }};
const sessionId = '{{ session_id }}';
let answerKeyData = new Map();
let miscellaneousQuestions = [];
async function initializeTomSelect() {
try {
const response = await fetch('/get_metadata_suggestions');
const suggestions = await response.json();
const subjectOptions = suggestions.subjects.map(s => ({value: s, text: s}));
const tagOptions = suggestions.tags.map(t => ({value: t, text: t}));
new TomSelect('#pdf_subject',{
create: true,
persist: false,
options: subjectOptions
});
new TomSelect('#pdf_tags',{
persist: false,
createOnBlur: true,
create: true,
plugins: ['remove_button'],
options: tagOptions
});
} catch (err) {
console.error('Error initializing Tom Select:', err);
}
}
function saveSettings() {
// Session-specific settings
const sessionSettings = {
pdf_subject: document.getElementById('pdf_subject').value,
pdf_tags: document.getElementById('pdf_tags').value,
pdf_notes: document.getElementById('pdf_notes').value,
pdf_name: document.getElementById('pdf_name').value,
};
localStorage.setItem('pdfGeneratorSettings-{{ session_id }}', JSON.stringify(sessionSettings));
// Global layout settings
const globalLayoutSettings = {
images_per_page: document.getElementById('images_per_page').value,
orientation: document.getElementById('orientation').value,
grid_rows: document.getElementById('grid_rows').value,
grid_cols: document.getElementById('grid_cols').value,
filter_type: document.getElementById('filter_type').value,
practice_mode: document.getElementById('practice_mode').value,
font_size_scale: document.getElementById('font_size_scale').value
};
localStorage.setItem('pdfGeneratorLayoutSettings', JSON.stringify(globalLayoutSettings));
}
function loadSettings() {
// Load session-specific settings (subject, tags, etc.)
const sessionSettings = JSON.parse(localStorage.getItem('pdfGeneratorSettings-{{ session_id }}'));
if (sessionSettings) {
const pdfSubjectInput = document.getElementById('pdf_subject');
if (!pdfSubjectInput.value) {
pdfSubjectInput.value = sessionSettings.pdf_subject || '';
}
const pdfNameInput = document.getElementById('pdf_name');
if (!pdfNameInput.value) {
pdfNameInput.value = sessionSettings.pdf_name || 'My-Test-Analysis';
}
const pdfTagsInput = document.getElementById('pdf_tags');
if (!pdfTagsInput.value) {
pdfTagsInput.value = sessionSettings.pdf_tags || '';
}
const pdfNotesInput = document.getElementById('pdf_notes');
if (!pdfNotesInput.value) {
pdfNotesInput.value = sessionSettings.pdf_notes || '';
}
}
// Load global layout settings
const globalLayoutSettings = JSON.parse(localStorage.getItem('pdfGeneratorLayoutSettings'));
if (globalLayoutSettings) {
document.getElementById('images_per_page').value = globalLayoutSettings.images_per_page || 4;
document.getElementById('orientation').value = globalLayoutSettings.orientation || 'portrait';
document.getElementById('grid_rows').value = globalLayoutSettings.grid_rows || 4;
document.getElementById('grid_cols').value = globalLayoutSettings.grid_cols || 1;
document.getElementById('filter_type').value = globalLayoutSettings.filter_type || 'all';
document.getElementById('practice_mode').value = globalLayoutSettings.practice_mode || 'none';
if (globalLayoutSettings.font_size_scale) {
document.getElementById('font_size_scale').value = globalLayoutSettings.font_size_scale;
document.getElementById('font_size_val').innerText = globalLayoutSettings.font_size_scale;
}
}
handlePracticeModeChange(); // Apply visibility changes on load
}
function handlePracticeModeChange() {
const practiceMode = document.getElementById('practice_mode').value;
const standardOptions = document.getElementById('standard-layout-options');
const orientationSelect = document.getElementById('orientation');
const pdfNameDiv = document.getElementById('pdf-name-div');
if (practiceMode === 'none') {
standardOptions.style.display = 'block';
pdfNameDiv.style.display = 'block';
} else {
standardOptions.style.display = 'none';
pdfNameDiv.style.display = 'none';
if (practiceMode === 'landscape_2') {
orientationSelect.value = 'landscape';
} else {
orientationSelect.value = 'portrait';
}
}
}
function updateGridDefaults() {
const imagesPerPage = parseInt(document.getElementById('images_per_page').value, 10);
const orientation = document.getElementById('orientation').value;
const rowsInput = document.getElementById('grid_rows');
const colsInput = document.getElementById('grid_cols');
if (imagesPerPage === 4) {
if (orientation === 'portrait') {
rowsInput.value = 4;
colsInput.value = 1;
} else { // landscape
rowsInput.value = 2;
colsInput.value = 2;
}
} else {
const cols = Math.ceil(Math.sqrt(imagesPerPage));
const rows = Math.ceil(imagesPerPage / cols);
rowsInput.value = rows;
colsInput.value = cols;
}
}
function setupEventListeners() {
document.getElementById('json-upload').addEventListener('change', handleJsonUpload);
document.getElementById('images_per_page').addEventListener('change', updateGridDefaults);
document.getElementById('orientation').addEventListener('change', updateGridDefaults);
document.getElementById('practice_mode').addEventListener('change', handlePracticeModeChange);
document.querySelectorAll('input[id^="question_number_"]').forEach(input => {
input.addEventListener('change', handleQuestionNumberChange);
});
document.addEventListener('keydown', handleShortcuts);
document.querySelectorAll('.status-buttons').forEach(setupStatusButtons);
document.getElementById('questions-form').addEventListener('submit', handleFormSubmit);
document.getElementById('preview-btn').addEventListener('click', handlePreview);
document.getElementById('add-misc-question').addEventListener('click', handleAddMiscQuestion);
document.getElementById('extract-classify-all').addEventListener('click', async () => {
const extractBtn = document.getElementById('extract-classify-all');
const spinner = extractBtn.querySelector('.spinner-border');
const text = extractBtn.querySelector('.extract-text');
spinner.classList.remove('d-none');
text.textContent = 'Processing...';
extractBtn.disabled = true;
// First, save the questions
const questions = [];
document.querySelectorAll('fieldset[data-question-index]').forEach((fieldset, i) => {
questions.push({
image_id: fieldset.querySelector('input[name^="image_id_"]').value,
question_number: fieldset.querySelector('input[name^="question_number_"]').value,
status: fieldset.querySelector('select[name^="status_"]').value,
marked_solution: fieldset.querySelector('input[name^="marked_solution_"]').value,
actual_solution: fieldset.querySelector('input[name^="actual_solution_"]').value
});
});
try {
const saveResponse = await fetch('/save_questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
questions: questions,
pdf_subject: document.getElementById('pdf_subject').value,
pdf_tags: document.getElementById('pdf_tags').value,
pdf_notes: document.getElementById('pdf_notes').value
})
}); if (!saveResponse.ok) throw new Error((await saveResponse.json()).error || 'Failed to save questions.');
// Now, run the classification
const response = await fetch(`/extract_and_classify_all/${sessionId}`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showStatus(result.message, 'success');
// Reload the page to show the updated data
setTimeout(() => {
location.reload();
}, 2000);
} else {
showStatus(`Error: ${result.error}`, 'danger');
}
} catch (err) {
showStatus(`Error: ${err.message}`, 'danger');
} finally {
spinner.classList.add('d-none');
text.textContent = 'Extract & Classify All';
extractBtn.disabled = false;
}
});
// Add event listeners for auto-extract buttons if NVIDIA NIM is available
{% if nvidia_nim_available %}
document.querySelectorAll('.auto-extract-btn').forEach(button => {
button.addEventListener('click', function() {
const imageId = this.dataset.imageId;
const index = this.dataset.index;
autoExtractQuestionNumber(this, imageId, index);
});
});
// Add event listener for auto-extract all button
document.getElementById('auto-extract-all').addEventListener('click', autoExtractAllQuestionNumbers);
// Add event listeners for delete buttons
document.querySelectorAll('.delete-question-btn').forEach(button => {
button.addEventListener('click', function() {
const imageId = this.dataset.imageId;
const index = this.dataset.index;
deleteQuestion(imageId, index);
});
});
{% endif %}
}
function handleJsonUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const json = JSON.parse(e.target.result);
if (!json.data || !Array.isArray(json.data)) throw new Error('Invalid format');
answerKeyData.clear();
json.data.forEach(item => answerKeyData.set(String(item.question_number), String(item.answer)));
showStatus(`Loaded ${answerKeyData.size} answers from key.`, 'success');
// Auto-fill answers for existing question numbers
document.querySelectorAll('input[id^="question_number_"]').forEach(input => {
if(input.value) handleQuestionNumberChange.call(input);
});
} catch (err) {
showStatus('Error reading or parsing JSON file.', 'danger');
}
};
reader.readAsText(file);
}
function handleQuestionNumberChange() {
const qNum = this.value;
if (answerKeyData.has(qNum)) {
const index = this.id.split('_').pop();
document.getElementById(`actual_solution_${index}`).value = answerKeyData.get(qNum);
}
}
function getCurrentQuestionIndex() {
const activeElement = document.activeElement;
const parentFieldset = activeElement?.closest('fieldset[data-question-index]');
return parentFieldset ? parseInt(parentFieldset.dataset.questionIndex, 10) : -1;
}
function setStatusForCurrentQuestion(status) {
const index = getCurrentQuestionIndex();
if (index !== -1) {
document.querySelector(`.status-buttons[data-index='${index}'] .status-btn[data-status='${status}']`)?.click();
}
}
function moveToNextQuestion() {
let index = getCurrentQuestionIndex();
if (index === -1) index = 0;
const nextIndex = (index + 1) % numImages;
document.getElementById(`question_number_${nextIndex}`)?.focus();
}
function handleShortcuts(e) {
if (e.shiftKey) {
const key = e.key.toLowerCase();
if (['q', 'c', 'w', 'u'].includes(key)) e.preventDefault();
switch(key) {
case 'q': moveToNextQuestion(); break;
case 'c': setStatusForCurrentQuestion('correct'); break;
case 'w': setStatusForCurrentQuestion('wrong'); break;
case 'u': setStatusForCurrentQuestion('unattempted'); break;
}
}
}
function setupStatusButtons(group) {
const buttons = group.querySelectorAll('.status-btn');
const index = group.dataset.index;
const select = document.getElementById(`status_${index}`);
buttons.forEach(btn => {
btn.addEventListener('click', () => {
buttons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
select.value = btn.dataset.status;
});
});
}
async function handlePreview(e) {
e.preventDefault();
saveSettings(); // Save settings for consistency
const statusDiv = document.getElementById('status');
showStatus('Generating preview...', 'info');
const questions = [];
document.querySelectorAll('fieldset[data-question-index]').forEach((fieldset, i) => {
questions.push({
image_id: fieldset.querySelector('input[name^="image_id_"]').value,
question_number: fieldset.querySelector('input[name^="question_number_"]').value,
status: fieldset.querySelector('select[name^="status_"]').value,
marked_solution: fieldset.querySelector('input[name^="marked_solution_"]').value,
actual_solution: fieldset.querySelector('input[name^="actual_solution_"]').value,
});
});
try {
const response = await fetch('/generate_preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
subject: document.getElementById('pdf_subject').value,
images_per_page: parseInt(document.getElementById('images_per_page').value, 10),
filter_type: document.getElementById('filter_type').value,
orientation: document.getElementById('orientation').value,
grid_rows: parseInt(document.getElementById('grid_rows').value, 10),
grid_cols: parseInt(document.getElementById('grid_cols').value, 10),
practice_mode: document.getElementById('practice_mode').value,
font_size_scale: parseFloat(document.getElementById('font_size_scale').value),
miscellaneous_questions: miscellaneousQuestions,
questions: questions // Pass the main questions too
})
});
const result = await response.json();
if (result.error) throw new Error(result.error);
const previewImg = document.getElementById('preview-img');
previewImg.src = result.preview_image;;
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
previewModal.show();
showStatus('Preview generated.', 'success');
} catch (err) {
showStatus(`Error: ${err.message}`, 'danger');
}
}
async function handleFormSubmit(e) {
e.preventDefault();
saveSettings(); // Save settings on submit
const statusDiv = document.getElementById('status');
showStatus('Saving questions and generating PDF...', 'info');
const questions = [];
document.querySelectorAll('fieldset[data-question-index]').forEach((fieldset, i) => {
questions.push({
image_id: fieldset.querySelector('input[name^="image_id_"]').value,
question_number: fieldset.querySelector('input[name^="question_number_"]').value,
status: fieldset.querySelector('select[name^="status_"]').value,
marked_solution: fieldset.querySelector('input[name^="marked_solution_"]').value,
actual_solution: fieldset.querySelector('input[name^="actual_solution_"]').value
});
});
try {
// First, save the question data.
const saveResponse = await fetch('/save_questions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
questions: questions,
pdf_subject: document.getElementById('pdf_subject').value,
pdf_tags: document.getElementById('pdf_tags').value,
pdf_notes: document.getElementById('pdf_notes').value
})
});
if (!saveResponse.ok) throw new Error((await saveResponse.json()).error || 'Failed to save questions.');
// Then, generate the PDF.
const pdfResponse = await fetch('/generate_pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
pdf_name: document.getElementById('pdf_name').value,
subject: document.getElementById('pdf_subject').value,
tags: document.getElementById('pdf_tags').value,
notes: document.getElementById('pdf_notes').value,
images_per_page: parseInt(document.getElementById('images_per_page').value, 10),
filter_type: document.getElementById('filter_type').value,
orientation: document.getElementById('orientation').value,
grid_rows: parseInt(document.getElementById('grid_rows').value, 10),
grid_cols: parseInt(document.getElementById('grid_cols').value, 10),
practice_mode: document.getElementById('practice_mode').value,
font_size_scale: parseFloat(document.getElementById('font_size_scale').value),
miscellaneous_questions: miscellaneousQuestions
})
});
const result = await pdfResponse.json();
if (result.error) throw new Error(result.error);
statusDiv.innerHTML = `<a href="/download/${result.pdf_filename}" class="btn btn-success w-100">Download PDF: ${result.pdf_filename}</a>`;
} catch (err) {
showStatus(`Error: ${err.message}`, 'danger');
}
}
function showStatus(message, type = 'info', duration = 4000) {
const statusContainer = document.getElementById('status');
const alertId = `alert-${Date.now()}`;
const alert = `<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>`;
statusContainer.innerHTML = alert;
}
function handleAddMiscQuestion() {
const imageInput = document.getElementById('misc_image');
const file = imageInput.files[0];
const question = {
id: `misc_${Date.now()}`,
question_number: document.getElementById('misc_question_number').value,
subject: document.getElementById('misc_subject').value,
status: document.getElementById('misc_status').value,
marked_solution: document.getElementById('misc_marked_solution').value,
actual_solution: document.getElementById('misc_actual_solution').value,
image_data: null
};
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
question.image_data = e.target.result;
miscellaneousQuestions.push(question);
renderMiscellaneousQuestions();
document.getElementById('misc-question-form').reset();
};
reader.readAsDataURL(file);
} else {
miscellaneousQuestions.push(question);
renderMiscellaneousQuestions();
document.getElementById('misc-question-form').reset();
}
}
function renderMiscellaneousQuestions() {
const listDiv = document.getElementById('misc-questions-list');
listDiv.innerHTML = '';
miscellaneousQuestions.forEach((q, index) => {
const item = document.createElement('div');
item.className = 'alert alert-secondary';
item.innerHTML = `
<strong>Q: ${q.question_number}</strong> - ${q.subject}
<button type="button" class="btn-close float-end" data-index="${index}"></button>
`;
listDiv.appendChild(item);
});
document.querySelectorAll('#misc-questions-list .btn-close').forEach(button => {
button.addEventListener('click', handleRemoveMiscQuestion);
});
}
function handleRemoveMiscQuestion(event) {
const index = event.target.dataset.index;
miscellaneousQuestions.splice(index, 1);
renderMiscellaneousQuestions();
}
// Auto-extract question number function
async function autoExtractQuestionNumber(button, imageId, index) {
// Check if NVIDIA NIM is available
{% if not nvidia_nim_available %}
showStatus('NVIDIA NIM OCR feature is not available. Please set the NVIDIA_API_KEY environment variable.', 'warning');
return;
{% endif %}
const spinner = button.querySelector('.extract-spinner');
const text = button.querySelector('.extract-text');
const questionInput = document.getElementById(`question_number_${index}`);
// Show loading state
spinner.classList.remove('d-none');
text.classList.add('d-none');
button.disabled = true;
try {
const response = await fetch('/extract_question_number', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_id: imageId })
});
const result = await response.json();
if (result.success) {
if (result.question_number) {
questionInput.value = result.question_number;
showStatus(`Question number ${result.question_number} extracted successfully!`, 'success');
} else {
showStatus('No question number found in the image.', 'warning');
}
} else {
showStatus(`Error: ${result.error}`, 'danger');
}
} catch (err) {
showStatus(`Error extracting question number: ${err.message}`, 'danger');
} finally {
// Restore button state
spinner.classList.add('d-none');
text.classList.remove('d-none');
button.disabled = false;
}
}
// Auto-extract all question numbers function
async function autoExtractAllQuestionNumbers() {
// Check if NVIDIA NIM is available
{% if not nvidia_nim_available %}
showStatus('NVIDIA NIM OCR feature is not available. Please set the NVIDIA_API_KEY environment variable.', 'warning');
return;
{% endif %}
const extractAllButton = document.getElementById('auto-extract-all');
const extractAllSpinner = extractAllButton.querySelector('.spinner-border');
const extractAllText = extractAllButton.querySelector('.extract-text');
// Show loading state for the main button
extractAllSpinner.classList.remove('d-none');
extractAllText.textContent = 'Extracting...';
extractAllButton.disabled = true;
try {
const response = await fetch('/extract_all_question_numbers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
const result = await response.json();
if (result.success) {
// Process successful results
result.results.forEach(item => {
if (item.question_number) {
// Find the correct input by using the data-image-id attribute
const questionInput = document.querySelector(`input[data-image-id="${item.image_id}"]`);
if (questionInput) {
questionInput.value = item.question_number;
}
}
});
// Show status message
let message = `Successfully extracted ${result.results.length} question numbers.`;
if (result.errors && result.errors.length > 0) {
message += ` Failed to extract ${result.errors.length} questions.`;
}
showStatus(message, 'success');
// Show individual errors if any
if (result.errors && result.errors.length > 0) {
result.errors.forEach(error => {
showStatus(`Error for image ${error.image_id}: ${error.error}`, 'danger');
});
}
} else {
showStatus(`Error: ${result.error}`, 'danger');
}
} catch (err) {
showStatus(`Error extracting question numbers: ${err.message}`, 'danger');
} finally {
// Restore button state
extractAllSpinner.classList.add('d-none');
extractAllText.textContent = 'Auto Extract All Question Numbers';
extractAllButton.disabled = false;
}
}
// Delete question function
async function deleteQuestion(imageId, index) {
if (!confirm('Are you sure you want to delete this question? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`/delete_question/${imageId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
// Remove the fieldset from the DOM
const fieldset = document.getElementById(`question-fieldset-${imageId}`);
if (fieldset) {
fieldset.remove();
}
showStatus('Question deleted successfully!', 'success');
} else {
showStatus(`Error: ${result.error}`, 'danger');
}
} catch (err) {
showStatus(`Error deleting question: ${err.message}`, 'danger');
}
}
document.addEventListener('DOMContentLoaded', () => {
initializeTomSelect(); // Initialize Tom Select here
loadSettings();
setupEventListeners();
});
</script>
{% endblock %}