Spaces:
Sleeping
Sleeping
| {% extends "base.html" %} | |
| {% block title %}Upload PDF{% endblock %} | |
| {% block head %} | |
| <!-- Ensure mobile viewport settings are correct --> | |
| <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; | |
| --input-bg: #2c3034; | |
| --border-color: #495057; | |
| --primary-color: #0d6efd; | |
| --touch-target: 48px; | |
| } | |
| body { | |
| background-color: var(--app-bg); | |
| color: #e9ecef; | |
| } | |
| /* Full height container with dynamic viewport support */ | |
| .upload-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 20px rgba(0,0,0,0.4); | |
| width: 100%; | |
| max-width: 600px; | |
| overflow: hidden; | |
| } | |
| /* Custom segmented tabs */ | |
| .nav-pills { | |
| background: var(--input-bg); | |
| padding: 4px; | |
| border-radius: 12px; | |
| gap: 4px; | |
| } | |
| .nav-pills .nav-link { | |
| border-radius: 8px; | |
| color: #adb5bd ; | |
| font-size: 0.9rem; | |
| padding: 10px; | |
| transition: all 0.2s; | |
| } | |
| .nav-pills .nav-link.active { | |
| background-color: var(--primary-color) ; | |
| color: white ; | |
| font-weight: 600; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.2); | |
| } | |
| /* File Drop Zone Styling */ | |
| .file-drop-area { | |
| position: relative; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| width: 100%; | |
| max-width: 100%; | |
| padding: 30px; | |
| border: 2px dashed var(--border-color); | |
| border-radius: 12px; | |
| transition: 0.2s; | |
| background: rgba(255,255,255,0.02); | |
| cursor: pointer; | |
| } | |
| .file-drop-area:hover, .file-drop-area.drag-over { | |
| border-color: var(--primary-color); | |
| background: rgba(13, 110, 253, 0.05); | |
| } | |
| .fake-btn { | |
| background: var(--input-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 8px 16px; | |
| font-size: 0.9rem; | |
| margin-top: 10px; | |
| } | |
| .file-msg { | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| color: #dee2e6; | |
| text-align: center; | |
| } | |
| .file-input { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| height: 100%; | |
| width: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| /* Large touch-friendly inputs */ | |
| .form-control-lg { | |
| background-color: var(--input-bg); | |
| border-color: var(--border-color); | |
| color: white; | |
| font-size: 16px; /* Prevents iOS zoom */ | |
| min-height: var(--touch-target); | |
| } | |
| .form-control-lg:focus { | |
| background-color: var(--input-bg); | |
| border-color: var(--primary-color); | |
| color: white; | |
| box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25); | |
| } | |
| /* Main Action Button */ | |
| .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); | |
| } | |
| /* List items for existing PDFs */ | |
| .list-group-item { | |
| padding: 16px; | |
| min-height: 54px; /* Touch target size */ | |
| border-color: var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .list-group-item:active { | |
| background-color: rgba(255,255,255,0.1) ; | |
| } | |
| /* Animations */ | |
| .fade-in-up { | |
| animation: fadeInUp 0.4s ease-out forwards; | |
| } | |
| @keyframes fadeInUp { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="upload-wrapper"> | |
| <div class="main-card fade-in-up"> | |
| <!-- Header --> | |
| <div class="p-4 border-bottom border-secondary" style="border-color: #495057 !important;"> | |
| <div class="text-center"> | |
| <div class="bg-primary bg-opacity-10 text-primary d-inline-flex align-items-center justify-content-center rounded-circle mb-3" style="width: 56px; height: 56px;"> | |
| <i class="bi bi-file-earmark-pdf-fill fs-3"></i> | |
| </div> | |
| <h1 class="h4 mb-1">Analyze PDF</h1> | |
| <p class="text-secondary small mb-0">Upload a document to extract questions</p> | |
| </div> | |
| </div> | |
| <div class="card-body p-4"> | |
| <!-- Navigation Tabs --> | |
| <ul class="nav nav-pills nav-fill mb-4" id="uploadMethodTab" role="tablist"> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link active" id="file-tab" data-bs-toggle="tab" data-bs-target="#file-upload-pane" type="button" role="tab" aria-controls="file-upload-pane" aria-selected="true"> | |
| <i class="bi bi-folder2-open me-1"></i> File | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url-upload-pane" type="button" role="tab" aria-controls="url-upload-pane" aria-selected="false"> | |
| <i class="bi bi-link-45deg me-1"></i> URL | |
| </button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="curl-tab" data-bs-toggle="tab" data-bs-target="#curl-upload-pane" type="button" role="tab" aria-controls="curl-upload-pane" aria-selected="false"> | |
| <i class="bi bi-code-slash me-1"></i> cURL | |
| </button> | |
| </li> | |
| </ul> | |
| <form id="upload-form"> | |
| <div class="tab-content mb-4"> | |
| <!-- File Upload Pane --> | |
| <div class="tab-pane fade show active" id="file-upload-pane" role="tabpanel" aria-labelledby="file-tab"> | |
| <div class="file-drop-area" id="drop-zone"> | |
| <span class="file-msg" id="file-label-text">Tap to select or drag PDF here</span> | |
| <span class="fake-btn">Choose File</span> | |
| <input class="file-input" type="file" id="pdf-upload" name="pdf" accept=".pdf"> | |
| </div> | |
| </div> | |
| <!-- URL Pane --> | |
| <div class="tab-pane fade" id="url-upload-pane" role="tabpanel" aria-labelledby="url-tab"> | |
| <div class="mb-3"> | |
| <label for="pdf-url" class="form-label small text-secondary">PDF URL Address</label> | |
| <input type="url" class="form-control form-control-lg" id="pdf-url" name="pdf_url" placeholder="https://example.com/doc.pdf or Google Drive Link" autocomplete="off"> | |
| <div class="form-text text-muted small mt-1"><i class="bi bi-google"></i> Google Drive sharing links supported</div> | |
| </div> | |
| </div> | |
| <!-- cURL Pane --> | |
| <div class="tab-pane fade" id="curl-upload-pane" role="tabpanel" aria-labelledby="curl-tab"> | |
| <div class="mb-3"> | |
| <label for="curl-command" class="form-label small text-secondary">Paste cURL Command</label> | |
| <textarea class="form-control form-control-lg" id="curl-command" name="curl_command" placeholder="curl -o 'report.pdf' ..." rows="2" style="font-family: monospace; font-size: 14px;"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Action Button --> | |
| <button type="submit" class="btn btn-primary w-100 btn-action shadow"> | |
| <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> | |
| <span class="btn-text">Start Processing</span> | |
| <i class="bi bi-arrow-right-circle btn-icon"></i> | |
| </button> | |
| </form> | |
| <div id="status" class="mt-3"></div> | |
| <!-- Alternative Actions --> | |
| <div class="mt-4 pt-3 border-top border-secondary" style="border-color: #343a40 !important;"> | |
| <div class="d-grid gap-2"> | |
| <a href="/images" class="btn btn-outline-secondary d-flex align-items-center justify-content-center" style="min-height: 48px;"> | |
| <i class="bi bi-images me-2"></i> Upload Images Instead | |
| </a> | |
| <button class="btn btn-dark d-flex align-items-center justify-content-between px-3" type="button" data-bs-toggle="collapse" data-bs-target="#existingPdfSection" aria-expanded="false" style="min-height: 48px; border: 1px solid var(--border-color);"> | |
| <span><i class="bi bi-clock-history me-2"></i> Recent Files</span> | |
| <i class="bi bi-chevron-down small"></i> | |
| </button> | |
| </div> | |
| <div class="collapse mt-2" id="existingPdfSection"> | |
| <div class="card card-body bg-dark border border-secondary p-0 overflow-hidden"> | |
| <div class="list-group list-group-flush" style="max-height: 200px; overflow-y: auto;"> | |
| {% if pdfs %} | |
| {% for pdf in pdfs %} | |
| <a href="/process_final_pdf/{{ pdf.id }}" class="list-group-item list-group-item-action bg-transparent text-white border-secondary"> | |
| <div class="text-truncate me-2">{{ pdf.filename }}</div> | |
| <i class="bi bi-chevron-right text-secondary small"></i> | |
| </a> | |
| {% endfor %} | |
| {% else %} | |
| <div class="p-3 text-center text-muted small">No recent files found.</div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| // --- File Input UX --- | |
| const fileInput = document.getElementById('pdf-upload'); | |
| const dropZone = document.getElementById('drop-zone'); | |
| const fileLabel = document.getElementById('file-label-text'); | |
| // Visual feedback for file selection | |
| fileInput.addEventListener('change', function() { | |
| if (this.files && this.files.length > 0) { | |
| fileLabel.innerHTML = `<i class="bi bi-check-circle-fill text-success"></i> ${this.files[0].name}`; | |
| dropZone.style.borderColor = '#198754'; | |
| dropZone.style.background = 'rgba(25, 135, 84, 0.1)'; | |
| } else { | |
| fileLabel.textContent = "Tap to select or drag PDF here"; | |
| dropZone.style.borderColor = ''; | |
| dropZone.style.background = ''; | |
| } | |
| }); | |
| // Drag and drop visual effects | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dropZone.classList.add('drag-over'); | |
| }, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropZone.addEventListener(eventName, (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| dropZone.classList.remove('drag-over'); | |
| }, false); | |
| }); | |
| // --- Paste Support --- | |
| document.addEventListener('paste', function(event) { | |
| const items = (event.clipboardData || event.originalEvent.clipboardData).items; | |
| for (let index in items) { | |
| const item = items[index]; | |
| if (item.kind === 'file') { | |
| const blob = item.getAsFile(); | |
| if (blob.type === 'application/pdf') { | |
| // It's a PDF! | |
| // We need to create a FileList-like object or use DataTransfer to set input.files | |
| const dataTransfer = new DataTransfer(); | |
| dataTransfer.items.add(blob); | |
| fileInput.files = dataTransfer.files; | |
| // Trigger change event to update UI | |
| const changeEvent = new Event('change'); | |
| fileInput.dispatchEvent(changeEvent); | |
| // Switch to file tab if not active | |
| const fileTabBtn = document.querySelector('#file-tab'); | |
| if (!fileTabBtn.classList.contains('active')) { | |
| const tab = new bootstrap.Tab(fileTabBtn); | |
| tab.show(); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| }); | |
| // --- Tab Handling --- | |
| const tabs = document.querySelectorAll('#uploadMethodTab .nav-link'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('shown.bs.tab', function (event) { | |
| const currentTabId = event.target.id; | |
| document.getElementById('pdf-upload').required = (currentTabId === 'file-tab'); | |
| document.getElementById('pdf-url').required = (currentTabId === 'url-tab'); | |
| document.getElementById('curl-command').required = (currentTabId === 'curl-tab'); | |
| }); | |
| }); | |
| // Set initial required state | |
| document.getElementById('pdf-upload').required = true; | |
| // --- Form Submission --- | |
| document.getElementById('upload-form').addEventListener('submit', async function(e) { | |
| e.preventDefault(); | |
| const form = e.target; | |
| const button = form.querySelector('button[type="submit"]'); | |
| const spinner = button.querySelector('.spinner-border'); | |
| const btnText = button.querySelector('.btn-text'); | |
| const btnIcon = button.querySelector('.btn-icon'); | |
| const statusDiv = document.getElementById('status'); | |
| // Reset UI | |
| statusDiv.innerHTML = ''; | |
| button.disabled = true; | |
| spinner.classList.remove('d-none'); | |
| btnIcon.classList.add('d-none'); | |
| btnText.textContent = "Uploading..."; | |
| const activeTab = document.querySelector('#uploadMethodTab .nav-link.active').id; | |
| const formData = new FormData(); | |
| // Validation & Data Prep | |
| let isValid = false; | |
| if (activeTab === 'file-tab') { | |
| const fileInput = document.getElementById('pdf-upload'); | |
| if (fileInput.files.length > 0) { | |
| formData.append('pdf', fileInput.files[0]); | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please select a PDF file.</div>`; | |
| } | |
| } else if (activeTab === 'url-tab') { | |
| const urlInput = document.getElementById('pdf-url'); | |
| if (urlInput.value) { | |
| formData.append('pdf_url', urlInput.value); | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please enter a PDF URL.</div>`; | |
| } | |
| } else if (activeTab === 'curl-tab') { | |
| const curlInput = document.getElementById('curl-command'); | |
| if (curlInput.value) { | |
| formData.append('curl_command', curlInput.value); | |
| isValid = true; | |
| } else { | |
| statusDiv.innerHTML = `<div class="alert alert-danger d-flex align-items-center"><i class="bi bi-exclamation-triangle-fill me-2"></i> Please enter a cURL command.</div>`; | |
| } | |
| } | |
| if (!isValid) { | |
| button.disabled = false; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Start Processing"; | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/v2/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (response.ok) { | |
| // Success State | |
| button.className = "btn btn-success w-100 btn-action shadow"; | |
| btnText.textContent = "Upload Complete!"; | |
| spinner.classList.add('d-none'); | |
| btnIcon.className = "bi bi-check-lg"; | |
| btnIcon.classList.remove('d-none'); | |
| statusDiv.innerHTML = ` | |
| <div class="alert alert-success border-0 bg-success bg-opacity-10 text-success"> | |
| <i class="bi bi-check-circle-fill me-2"></i> Found ${result.files.length} pages. | |
| </div> | |
| `; | |
| // Redirect immediately or show button? | |
| // UX Best practice: Show button to proceed or auto-redirect after short delay. | |
| // Let's replace the button with a "Start Cropping" link to be safe. | |
| setTimeout(() => { | |
| window.location.href = `/cropv2/${result.session_id}/0`; | |
| }, 500); // 500ms delay to see the checkmark | |
| } else { | |
| throw new Error(result.error || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| statusDiv.innerHTML = ` | |
| <div class="alert alert-danger border-0 bg-danger bg-opacity-10 text-danger"> | |
| <i class="bi bi-x-circle-fill me-2"></i> ${error.message} | |
| </div> | |
| `; | |
| button.disabled = false; | |
| button.className = "btn btn-primary w-100 btn-action shadow"; | |
| spinner.classList.add('d-none'); | |
| btnIcon.classList.remove('d-none'); | |
| btnText.textContent = "Try Again"; | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |