Report-Generator / templates /qtab_list.html
Jaimodiji's picture
Upload folder using huggingface_hub
c001f24
Raw
History Blame
23.2 kB
{% extends "base.html" %}
{% block title %}Question Table Generator{% endblock %}
{% block head %}
<style>
body {
padding-bottom: 100px; /* Extra space at bottom for device chrome */
}
.qtab-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
min-height: calc(100vh - 150px); /* Dynamic height with bottom space */
}
.upload-section {
background: #212529;
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
border: 2px dashed #495057;
text-align: center;
transition: all 0.3s;
}
.upload-section.dragover {
border-color: #0d6efd;
background: #1a1d20;
}
.upload-section.paste-active {
border-color: #28a745;
background: #1a2520;
}
.folder-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.folder-card {
background: #212529;
padding: 20px;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
border: 1px solid #495057;
position: relative;
}
.folder-card:hover {
background: #2c3136;
border-color: #0d6efd;
transform: translateY(-2px);
}
.folder-card i {
font-size: 48px;
color: #0d6efd;
margin-bottom: 10px;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.image-card {
background: #212529;
border-radius: 12px;
overflow: hidden;
border: 1px solid #495057;
transition: all 0.3s;
position: relative;
}
.image-card.selected {
border-color: #0d6efd;
box-shadow: 0 0 10px rgba(13, 110, 253, 0.5);
}
.image-card:hover {
border-color: #0d6efd;
transform: translateY(-2px);
}
.image-card .selection-checkbox {
position: absolute;
top: 10px;
left: 10px;
z-index: 10;
width: 20px;
height: 20px;
cursor: pointer;
}
.image-preview {
width: 100%;
height: 200px;
object-fit: cover;
background: #343a40;
}
.image-content {
padding: 15px;
}
.result-section {
margin-top: 10px;
padding: 10px;
background: #343a40;
border-radius: 6px;
max-height: 300px;
overflow-y: auto;
}
.result-success {
border-left: 3px solid #28a745;
}
.result-error {
border-left: 3px solid #dc3545;
}
.section-group {
margin-bottom: 15px;
}
.section-title {
font-weight: bold;
color: #0d6efd;
margin-bottom: 5px;
}
.qa-pair {
display: flex;
justify-content: space-between;
padding: 5px;
margin: 2px 0;
background: #212529;
border-radius: 4px;
}
.upload-btn {
display: inline-block;
padding: 12px 30px;
background: #0d6efd;
color: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.upload-btn:hover {
background: #0b5ed7;
transform: scale(1.05);
}
#imageInput {
display: none;
}
.loading-spinner {
display: none;
margin: 20px auto;
}
.loading-spinner.active {
display: block;
}
/* Paste indicator */
.paste-hint {
font-size: 0.9em;
color: #6c757d;
margin-top: 10px;
}
.paste-hint .kbd {
background: #343a40;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
</style>
{% endblock %}
{% block content %}
<div class="qtab-container">
<!-- Toolbar -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2 class="mb-0"><i class="bi bi-table"></i> Question Table Generator</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="{{ url_for('qtab.qtab_list') }}">Home</a></li>
{% for crumb in breadcrumbs %}
<li class="breadcrumb-item"><a href="{{ url_for('qtab.qtab_list', folder_path=crumb.path) }}">{{ crumb.name }}</a></li>
{% endfor %}
</ol>
</nav>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" onclick="showCreateFolderModal()">
<i class="bi bi-folder-plus"></i> New Folder
</button>
<button type="button" class="btn btn-warning text-dark" id="moveBtn" disabled data-bs-toggle="modal" data-bs-target="#moveModal">
<i class="bi bi-arrow-right-square"></i> Move Selected
</button>
<button type="button" class="btn btn-danger" id="deleteBtn" disabled onclick="deleteSelectedImages()">
<i class="bi bi-trash"></i> Delete Selected
</button>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Upload Section -->
<div class="upload-section" id="uploadSection">
<i class="fas fa-cloud-upload-alt fa-3x mb-3"></i>
<h4>Upload Image for Question Extraction</h4>
<p class="text-muted">Drag and drop an image here, or click to select</p>
<label for="imageInput" class="upload-btn">
<i class="fas fa-upload"></i> Choose Image
</label>
<input type="file" id="imageInput" accept="image/*">
<div class="paste-hint">
<span class="kbd">Ctrl+V</span> to paste image from clipboard
</div>
<div class="spinner-border loading-spinner" role="status">
<span class="visually-hidden">Processing...</span>
</div>
</div>
<!-- Folders Section -->
{% if subfolders %}
<h3 class="mb-3"><i class="fas fa-folder"></i> Folders</h3>
<div class="folder-grid">
{% for folder in subfolders %}
<div class="folder-card" onclick="navigateToFolder('{{ folder.name }}')">
<i class="fas fa-folder"></i>
<div>{{ folder.name }}</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Images Section -->
{% if images %}
<h3 class="mb-3"><i class="fas fa-images"></i> Processed Images</h3>
<div class="images-grid" id="imagesGrid">
{% for image in images %}
<div class="image-card" data-image-id="{{ image.id }}">
<input type="checkbox" class="form-check-input selection-checkbox image-checkbox" value="{{ image.id }}">
<img src="{{ url_for('qtab.serve_qtab_image', filename=image.filename) }}"
class="image-preview"
alt="{{ image.original_name }}"
onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect fill=%22%23343a40%22 width=%22100%22 height=%22100%22/%3E%3Ctext fill=%22%236c757d%22 x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 dy=%22.3em%22%3ENo Preview%3C/text%3E%3C/svg%3E'">
<div class="image-content">
<div class="d-flex justify-content-between align-items-start">
<h6 class="mb-1">{{ image.original_name }}</h6>
<button class="btn btn-sm btn-link p-0" onclick="renameImage({{ image.id }}, '{{ image.original_name }}')" title="Rename">
<i class="bi bi-pencil"></i>
</button>
</div>
<small class="text-muted">{{ image.created_at }}</small>
{% if image.result_json %}
<div class="result-section result-{{ image.status }}">
<div id="result-{{ image.id }}"></div>
</div>
<script>
(function() {
try {
const result = {{ image.result_json | safe }};
const container = document.getElementById('result-{{ image.id }}');
if (result.status === 'success') {
if (Array.isArray(result.data)) {
// No sections
container.innerHTML = result.data.map(qa =>
`<div class="qa-pair"><span>Q${qa.question_number}</span><span><strong>${qa.answer}</strong></span></div>`
).join('');
} else {
// With sections
container.innerHTML = Object.entries(result.data).map(([section, questions]) =>
`<div class="section-group">
<div class="section-title">${section}</div>
${questions.map(qa =>
`<div class="qa-pair"><span>Q${qa.question_number}</span><span><strong>${qa.answer}</strong></span></div>`
).join('')}
</div>`
).join('');
}
} else {
container.innerHTML = `<div class="text-danger"><i class="fas fa-exclamation-triangle"></i> ${result.message || 'Processing failed'}</div>`;
}
} catch(e) {
console.error('Error rendering result:', e);
}
})();
</script>
{% endif %}
<div class="d-flex gap-2 mt-2">
<a href="{{ url_for('qtab.qtab_exam_mode', image_id=image.id) }}" class="btn btn-sm btn-primary flex-grow-1">
<i class="fas fa-graduation-cap"></i> Exam Mode
</a>
<button class="btn btn-sm btn-danger" onclick="deleteImage({{ image.id }})">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
{% endfor %}
</div>
{% elif not subfolders %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No images uploaded yet. Upload an image to get started!
</div>
{% endif %}
</div>
<!-- Create Folder Modal -->
<div class="modal fade" id="createFolderModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header">
<h5 class="modal-title">Create New Folder</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="folderName" class="form-label">Folder Name</label>
<input type="text" class="form-control" id="folderName" placeholder="Enter folder name">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createFolder()">Create</button>
</div>
</div>
</div>
</div>
<!-- Move Modal -->
<div class="modal fade" id="moveModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header">
<h5 class="modal-title">Move Images</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="targetFolder" class="form-label">Select Destination Folder</label>
<select class="form-select" id="targetFolder">
<option value="">Root</option>
{% if folder_tree %}
{% for folder in folder_tree %}
<option value="{{ folder.id }}">{{ folder.path }}</option>
{% endfor %}
{% endif %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-warning text-dark" onclick="moveSelectedImages()">Move</button>
</div>
</div>
</div>
</div>
<!-- Rename Image Modal -->
<div class="modal fade" id="renameModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content bg-dark">
<div class="modal-header">
<h5 class="modal-title">Rename Image</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newImageName" class="form-label">New Name</label>
<input type="text" class="form-control" id="newImageName" placeholder="Enter new name">
<input type="hidden" id="renameImageId">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitRename()">Rename</button>
</div>
</div>
</div>
</div>
<script>
const currentFolderId = {{ current_folder_id | tojson }};
const currentFolderPath = '{{ breadcrumbs[-1].path if breadcrumbs else "" }}';
// File upload handling
const uploadSection = document.getElementById('uploadSection');
const imageInput = document.getElementById('imageInput');
const loadingSpinner = document.querySelector('.loading-spinner');
imageInput.addEventListener('change', handleFileSelect);
uploadSection.addEventListener('dragover', (e) => {
e.preventDefault();
uploadSection.classList.add('dragover');
});
uploadSection.addEventListener('dragleave', () => {
uploadSection.classList.remove('dragover');
});
uploadSection.addEventListener('drop', (e) => {
e.preventDefault();
uploadSection.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
// Ctrl+V paste image support
document.addEventListener('paste', async (e) => {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
e.preventDefault();
const blob = items[i].getAsFile();
// Visual feedback
uploadSection.classList.add('paste-active');
setTimeout(() => uploadSection.classList.remove('paste-active'), 300);
handleFile(blob);
break;
}
}
});
function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
handleFile(file);
}
}
async function handleFile(file) {
if (!file.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
const formData = new FormData();
formData.append('image', file);
formData.append('folder_id', currentFolderId || '');
loadingSpinner.classList.add('active');
try {
const response = await fetch('{{ url_for("qtab.qtab_upload") }}', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.status === 'success' || result.status === 'error') {
// Reload page to show new image
window.location.reload();
} else {
alert('Upload failed: ' + (result.message || 'Unknown error'));
}
} catch (error) {
console.error('Upload error:', error);
alert('Upload failed: ' + error.message);
} finally {
loadingSpinner.classList.remove('active');
imageInput.value = '';
}
}
// Selection handling
const imageCheckboxes = document.querySelectorAll('.image-checkbox');
const moveBtn = document.getElementById('moveBtn');
const deleteBtn = document.getElementById('deleteBtn');
imageCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateSelectionButtons);
});
function updateSelectionButtons() {
const selectedCount = document.querySelectorAll('.image-checkbox:checked').length;
moveBtn.disabled = selectedCount === 0;
deleteBtn.disabled = selectedCount === 0;
// Update card styling
document.querySelectorAll('.image-card').forEach(card => {
const checkbox = card.querySelector('.image-checkbox');
if (checkbox && checkbox.checked) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
}
function navigateToFolder(folderName) {
const newPath = currentFolderPath ? `${currentFolderPath}/${folderName}` : folderName;
window.location.href = `{{ url_for('qtab.qtab_list') }}/${newPath}`;
}
function showCreateFolderModal() {
const modal = new bootstrap.Modal(document.getElementById('createFolderModal'));
modal.show();
}
async function createFolder() {
const folderName = document.getElementById('folderName').value.trim();
if (!folderName) {
alert('Please enter a folder name');
return;
}
try {
const response = await fetch('{{ url_for("qtab.qtab_create_folder") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: folderName,
parent_id: currentFolderId
})
});
const result = await response.json();
if (result.success) {
window.location.reload();
} else {
alert('Error: ' + (result.error || 'Failed to create folder'));
}
} catch (error) {
console.error('Error creating folder:', error);
alert('Error: ' + error.message);
}
}
// Rename image
function renameImage(imageId, currentName) {
document.getElementById('renameImageId').value = imageId;
document.getElementById('newImageName').value = currentName;
const modal = new bootstrap.Modal(document.getElementById('renameModal'));
modal.show();
}
async function submitRename() {
const imageId = document.getElementById('renameImageId').value;
const newName = document.getElementById('newImageName').value.trim();
if (!newName) {
alert('Please enter a name');
return;
}
try {
const response = await fetch('{{ url_for("qtab.qtab_rename_image", image_id=0) }}'.replace('0', imageId), {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: newName })
});
const result = await response.json();
if (result.success) {
window.location.reload();
} else {
alert('Error: ' + (result.error || 'Failed to rename image'));
}
} catch (error) {
console.error('Error renaming image:', error);
alert('Error: ' + error.message);
}
}
// Move selected images
async function moveSelectedImages() {
const selectedIds = Array.from(document.querySelectorAll('.image-checkbox:checked'))
.map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
alert('No images selected');
return;
}
const targetFolderId = document.getElementById('targetFolder').value || null;
try {
const response = await fetch('{{ url_for("qtab.qtab_move_images") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
image_ids: selectedIds,
target_folder_id: targetFolderId
})
});
const result = await response.json();
if (result.success) {
window.location.reload();
} else {
alert('Error: ' + (result.error || 'Failed to move images'));
}
} catch (error) {
console.error('Error moving images:', error);
alert('Error: ' + error.message);
}
}
// Delete selected images
async function deleteSelectedImages() {
const selectedIds = Array.from(document.querySelectorAll('.image-checkbox:checked'))
.map(cb => parseInt(cb.value));
if (selectedIds.length === 0) {
alert('No images selected');
return;
}
if (!confirm(`Are you sure you want to delete ${selectedIds.length} image(s)?`)) {
return;
}
try {
// Delete each image
for (const imageId of selectedIds) {
await fetch(`{{ url_for('qtab.qtab_delete_image', image_id=0) }}`.replace('0', imageId), {
method: 'DELETE'
});
}
window.location.reload();
} catch (error) {
console.error('Error deleting images:', error);
alert('Error: ' + error.message);
}
}
async function deleteImage(imageId) {
if (!confirm('Are you sure you want to delete this image?')) {
return;
}
try {
const response = await fetch(`{{ url_for('qtab.qtab_delete_image', image_id=0) }}`.replace('0', imageId), {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
// Remove card from DOM
const card = document.querySelector(`[data-image-id="${imageId}"]`);
if (card) {
card.remove();
}
} else {
alert('Error: ' + (result.error || 'Failed to delete image'));
}
} catch (error) {
console.error('Error deleting image:', error);
alert('Error: ' + error.message);
}
}
</script>
{% endblock %}