Spaces:
Running
Running
| {% extends "base.html" %} | |
| {% block title %}Subjective Question Generator{% endblock %} | |
| {% block head %} | |
| <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1, viewport-fit=cover"> | |
| <style> | |
| :root { | |
| --app-bg: #181a1c; | |
| --card-bg: #212529; | |
| --border-color: #495057; | |
| --primary-color: #0d6efd; | |
| --primary-bg-subtle: rgba(13, 110, 253, 0.1); | |
| } | |
| body { | |
| background-color: var(--app-bg); | |
| color: #e9ecef; | |
| } | |
| /* Centered Layout */ | |
| .generator-wrapper { | |
| min-height: calc(100vh - 56px); | |
| min-height: calc(100dvh - 56px); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 1rem; | |
| padding-bottom: env(safe-area-inset-bottom); | |
| } | |
| .main-card { | |
| background-color: var(--card-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.4); | |
| width: 100%; | |
| max-width: 600px; | |
| overflow: hidden; | |
| } | |
| /* Touch-friendly Upload Zone */ | |
| .upload-zone { | |
| position: relative; | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| padding: 2rem 1rem; | |
| text-align: center; | |
| background: rgba(255,255,255,0.02); | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| min-height: 250px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .upload-zone:active, .upload-zone.drag-over { | |
| border-color: var(--primary-color); | |
| background: var(--primary-bg-subtle); | |
| transform: scale(0.99); | |
| } | |
| .upload-zone.has-file { | |
| border-style: solid; | |
| border-color: #198754; | |
| background: rgba(25, 135, 84, 0.05); | |
| padding: 1rem; | |
| } | |
| /* Hidden Input */ | |
| .file-input { | |
| position: absolute; | |
| top: 0; left: 0; | |
| width: 100%; height: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| z-index: 10; | |
| } | |
| /* Icon & Text */ | |
| .upload-icon { | |
| font-size: 3rem; | |
| color: #adb5bd; | |
| margin-bottom: 1rem; | |
| transition: color 0.2s; | |
| } | |
| .upload-zone.has-file .upload-icon { | |
| display: none; | |
| } | |
| .zone-text { | |
| font-weight: 500; | |
| color: #e9ecef; | |
| } | |
| .zone-subtext { | |
| font-size: 0.85rem; | |
| color: #adb5bd; | |
| margin-top: 0.5rem; | |
| } | |
| /* Image Preview */ | |
| #image-preview { | |
| max-width: 100%; | |
| max-height: 300px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| display: none; /* Hidden by default */ | |
| margin-bottom: 1rem; | |
| } | |
| /* Button Styling */ | |
| .btn-action { | |
| min-height: 54px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| transition: transform 0.1s; | |
| } | |
| .btn-action:active { | |
| transform: scale(0.98); | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="generator-wrapper"> | |
| <div class="main-card"> | |
| <!-- Header --> | |
| <div class="p-4 border-bottom border-secondary" style="border-color: #495057 !important;"> | |
| <div class="d-flex align-items-center justify-content-center mb-2"> | |
| <div class="bg-primary bg-opacity-10 text-primary p-3 rounded-circle d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;"> | |
| <i class="bi bi-magic fs-3"></i> | |
| </div> | |
| </div> | |
| <div class="text-center"> | |
| <h1 class="h4 mb-1">Subjective Generator</h1> | |
| <p class="text-secondary small mb-0">AI will transcribe and format your handwritten or printed questions.</p> | |
| </div> | |
| </div> | |
| <div class="card-body p-4"> | |
| <!-- Remote Camera Component --> | |
| {% include 'camera_receiver_component.html' %} | |
| <form action="{{ url_for('subjective.generate') }}" method="POST" enctype="multipart/form-data" id="generateForm"> | |
| <!-- Upload Zone --> | |
| <div class="mb-4"> | |
| <div class="d-flex justify-content-between align-items-center mb-2"> | |
| <label class="form-label mb-0">Image</label> | |
| <button type="button" class="btn btn-sm btn-outline-info" onclick="toggleCameraReceiver()"> | |
| <i class="bi bi-camera-video me-1"></i> Use Remote Camera | |
| </button> | |
| </div> | |
| <div class="upload-zone" id="drop-zone"> | |
| <!-- Preview Image --> | |
| <img id="image-preview" alt="Preview"> | |
| <!-- Placeholder Content --> | |
| <div id="placeholder-content"> | |
| <i class="bi bi-cloud-upload upload-icon"></i> | |
| <div class="zone-text" id="main-text">Tap to upload image</div> | |
| <div class="zone-subtext" id="sub-text">Supports JPG, PNG (Max 10MB)</div> | |
| </div> | |
| <input type="file" accept="image/*" id="filePicker" class="file-input" name="image"> | |
| </div> | |
| </div> | |
| <!-- Manual JSON Input (Accordion) --> | |
| <div class="accordion mb-4" id="manualInputAccordion"> | |
| <div class="accordion-item bg-transparent border-secondary"> | |
| <h2 class="accordion-header" id="headingManual"> | |
| <button class="accordion-button collapsed bg-transparent text-secondary shadow-none" type="button" data-bs-toggle="collapse" data-bs-target="#collapseManual" aria-expanded="false" aria-controls="collapseManual"> | |
| <i class="bi bi-code-square me-2"></i> Or Paste JSON Manually | |
| </button> | |
| </h2> | |
| <div id="collapseManual" class="accordion-collapse collapse" aria-labelledby="headingManual" data-bs-parent="#manualInputAccordion"> | |
| <div class="accordion-body text-secondary"> | |
| <p class="small mb-2">If Gemini is unavailable, paste the raw JSON response here.</p> | |
| <textarea class="form-control bg-dark text-light border-secondary font-monospace small" name="json_data" id="json_data" rows="5" placeholder='[{"question_number_within_topic": "1", "question_topic": "Topic", "question_html": "..."}]'></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Button --> | |
| <button type="submit" class="btn btn-primary w-100 btn-action shadow" id="generateBtn"> | |
| <span id="btnSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <span id="btnText">Transcribe & Generate</span> | |
| <i class="bi bi-stars" id="btnIcon"></i> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| const fileInput = document.getElementById('filePicker'); | |
| const dropZone = document.getElementById('drop-zone'); | |
| const previewImg = document.getElementById('image-preview'); | |
| const placeholder = document.getElementById('placeholder-content'); | |
| const mainText = document.getElementById('main-text'); | |
| const subText = document.getElementById('sub-text'); | |
| const form = document.getElementById('generateForm'); | |
| function toggleCameraReceiver() { | |
| const component = document.getElementById('camera-receiver-component'); | |
| if (component.style.display === 'none') { | |
| component.style.display = 'block'; | |
| if (typeof initCameraReceiver === 'function') { | |
| initCameraReceiver(); | |
| } | |
| } else { | |
| component.style.display = 'none'; | |
| } | |
| } | |
| // --- File Selection & Preview --- | |
| fileInput.addEventListener('change', function(e) { | |
| const file = this.files[0]; | |
| if (file) { | |
| // Show file name | |
| dropZone.classList.add('has-file'); | |
| mainText.innerHTML = `<i class="bi bi-check-circle-fill text-success"></i> Selected`; | |
| subText.textContent = file.name; | |
| // Render Preview | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| previewImg.src = e.target.result; | |
| previewImg.style.display = 'block'; | |
| // Hide icon, keep text below | |
| document.querySelector('.upload-icon').style.display = 'none'; | |
| } | |
| reader.readAsDataURL(file); | |
| } else { | |
| resetUI(); | |
| } | |
| }); | |
| function resetUI() { | |
| dropZone.classList.remove('has-file'); | |
| previewImg.style.display = 'none'; | |
| previewImg.src = ''; | |
| document.querySelector('.upload-icon').style.display = 'block'; | |
| mainText.textContent = "Tap to upload image"; | |
| subText.textContent = "Supports JPG, PNG (Max 10MB)"; | |
| } | |
| // --- Drag & Drop Visuals --- | |
| ['dragenter', 'dragover'].forEach(evt => { | |
| dropZone.addEventListener(evt, (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.add('drag-over'); | |
| }); | |
| }); | |
| ['dragleave', 'drop'].forEach(evt => { | |
| dropZone.addEventListener(evt, (e) => { | |
| e.preventDefault(); | |
| dropZone.classList.remove('drag-over'); | |
| }); | |
| }); | |
| // --- Form Submission State --- | |
| form.addEventListener('submit', function(e) { | |
| const hasFile = fileInput.files && fileInput.files.length > 0; | |
| const hasJson = document.getElementById('json_data').value.trim().length > 0; | |
| if (!hasFile && !hasJson) { | |
| e.preventDefault(); | |
| alert("Please select an image or provide JSON data."); | |
| return; | |
| } | |
| const btn = document.getElementById('generateBtn'); | |
| const text = document.getElementById('btnText'); | |
| const icon = document.getElementById('btnIcon'); | |
| const spinner = document.getElementById('btnSpinner'); | |
| btn.disabled = true; | |
| btn.classList.add('opacity-75'); | |
| icon.classList.add('d-none'); | |
| spinner.classList.remove('d-none'); | |
| text.textContent = 'Processing...'; | |
| }); | |
| </script> | |
| {% endblock %} | |