Add drag-and-drop upload functionality to Files view
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
// frontend/static/js/upload.js
|
||||
// Reusable drag-and-drop upload functionality for DocuElevate
|
||||
|
||||
// Configuration
|
||||
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
|
||||
// Allowed file types
|
||||
const ACCEPTED_TYPES = {
|
||||
// PDF files
|
||||
'application/pdf': true,
|
||||
|
||||
// Image formats
|
||||
'image/jpeg': true, 'image/jpg': true, 'image/png': true,
|
||||
'image/gif': true, 'image/bmp': true, 'image/tiff': true,
|
||||
'image/webp': true, 'image/svg+xml': true,
|
||||
|
||||
// Office document formats - Word
|
||||
'application/msword': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': true,
|
||||
'application/vnd.ms-word.document.macroEnabled.12': true,
|
||||
|
||||
// Excel
|
||||
'application/vnd.ms-excel': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': true,
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12': true,
|
||||
|
||||
// PowerPoint
|
||||
'application/vnd.ms-powerpoint': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow': true,
|
||||
|
||||
// Other common formats
|
||||
'text/plain': true,
|
||||
'text/csv': true,
|
||||
'application/rtf': true,
|
||||
'text/rtf': true,
|
||||
'text/html': true,
|
||||
'application/xml': true,
|
||||
'text/xml': true
|
||||
};
|
||||
|
||||
// File extensions that are always allowed (even if mime type is not recognized)
|
||||
const ACCEPTED_EXTENSIONS = [
|
||||
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
||||
'.odt', '.ods', '.odp', '.rtf', '.txt', '.csv',
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg', '.md'
|
||||
];
|
||||
|
||||
/**
|
||||
* Process a list of files for upload
|
||||
* @param {FileList} files - Files to process
|
||||
* @param {HTMLElement} progressContainer - Container element for progress display
|
||||
* @param {HTMLElement} statusMessage - Element for status message display
|
||||
*/
|
||||
function processFiles(files, progressContainer, statusMessage) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
if (statusMessage) {
|
||||
statusMessage.textContent = `Processing ${files.length} file(s)...`;
|
||||
}
|
||||
|
||||
// Clear previous upload progress
|
||||
if (progressContainer) {
|
||||
progressContainer.innerHTML = "";
|
||||
}
|
||||
|
||||
// Process each file
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
validateAndUpload(file, progressContainer, statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and upload a single file
|
||||
* @param {File} file - File to validate and upload
|
||||
* @param {HTMLElement} progressContainer - Container element for progress display
|
||||
* @param {HTMLElement} statusMessage - Element for status message display
|
||||
*/
|
||||
function validateAndUpload(file, progressContainer, statusMessage) {
|
||||
// Create progress element for this file
|
||||
const fileProgress = document.createElement("div");
|
||||
fileProgress.className = "flex flex-col mb-2";
|
||||
fileProgress.innerHTML = `
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm truncate" title="${file.name}">${file.name}</span>
|
||||
<span class="text-xs text-gray-500">${formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded-full mt-1">
|
||||
<div class="file-progress-bar bg-blue-500 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="file-status text-xs text-gray-600 mt-1">Validating...</div>
|
||||
`;
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.appendChild(fileProgress);
|
||||
}
|
||||
|
||||
const progressBar = fileProgress.querySelector(".file-progress-bar");
|
||||
const statusEl = fileProgress.querySelector(".file-status");
|
||||
|
||||
// Validate file type by checking both MIME type and extension
|
||||
const isValidMimeType = ACCEPTED_TYPES[file.type] || false;
|
||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
const isValidExtension = ACCEPTED_EXTENSIONS.includes(fileExtension);
|
||||
|
||||
if (!isValidMimeType && !isValidExtension) {
|
||||
statusEl.textContent = `Error: ${file.name} - Unsupported file type`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
statusEl.textContent = `Error: ${file.name} - File size exceeds 500MB limit`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the file
|
||||
uploadFile(file, progressBar, statusEl, statusMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to the server
|
||||
* @param {File} file - File to upload
|
||||
* @param {HTMLElement} progressBar - Progress bar element
|
||||
* @param {HTMLElement} statusEl - Status element
|
||||
* @param {HTMLElement} statusMessage - Overall status message element
|
||||
*/
|
||||
async function uploadFile(file, progressBar, statusEl, statusMessage) {
|
||||
statusEl.textContent = `Uploading...`;
|
||||
try {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/ui-upload", true);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
progressBar.style.width = percentComplete + "%";
|
||||
statusEl.textContent = `Uploading: ${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
progressBar.style.width = "100%";
|
||||
progressBar.className = "file-progress-bar bg-green-500 h-2 rounded-full";
|
||||
statusEl.textContent = `Success: Task ID: ${result.task_id}`;
|
||||
statusEl.className = "text-xs text-green-600 mt-1";
|
||||
updateOverallStatus(statusMessage);
|
||||
} else {
|
||||
throw new Error(`Upload failed with status ${xhr.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
throw new Error("Network error occurred");
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
|
||||
} catch (err) {
|
||||
statusEl.textContent = `Error: ${err.message}`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
progressBar.className = "file-progress-bar bg-red-500 h-2 rounded-full";
|
||||
updateOverallStatus(statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the overall status message based on file statuses
|
||||
* @param {HTMLElement} statusMessage - Status message element
|
||||
*/
|
||||
function updateOverallStatus(statusMessage) {
|
||||
if (!statusMessage) return;
|
||||
|
||||
// Count success/failure
|
||||
const fileStatuses = document.querySelectorAll('.file-status');
|
||||
let completed = 0;
|
||||
let total = fileStatuses.length;
|
||||
|
||||
fileStatuses.forEach(status => {
|
||||
if (status.textContent.includes('Success') || status.textContent.includes('Error')) {
|
||||
completed++;
|
||||
}
|
||||
});
|
||||
|
||||
if (completed === total) {
|
||||
statusMessage.textContent = `All uploads completed (${completed}/${total})`;
|
||||
} else {
|
||||
statusMessage.textContent = `Uploading files (${completed}/${total})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size for display
|
||||
* @param {number} bytes - File size in bytes
|
||||
* @returns {string} Formatted file size
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize drag-and-drop on an element
|
||||
* @param {HTMLElement} element - Element to enable drag-and-drop on
|
||||
* @param {HTMLElement} progressContainer - Container for progress display
|
||||
* @param {HTMLElement} statusMessage - Element for status messages
|
||||
* @param {Object} options - Additional options
|
||||
*/
|
||||
function initDragAndDrop(element, progressContainer, statusMessage, options = {}) {
|
||||
if (!element) {
|
||||
console.error("Element not found for drag-and-drop initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add event listeners for drag-and-drop
|
||||
element.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
// Add visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.add(options.dragOverClass);
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener("dragleave", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Remove visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.remove(options.dragOverClass);
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Remove visual feedback
|
||||
if (options.dragOverClass) {
|
||||
element.classList.remove(options.dragOverClass);
|
||||
}
|
||||
|
||||
if (e.dataTransfer.files.length) {
|
||||
processFiles(e.dataTransfer.files, progressContainer, statusMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,86 @@
|
||||
|
||||
{% block head_extra %}
|
||||
<script src="/static/js/common.js"></script>
|
||||
<script src="/static/js/upload.js"></script>
|
||||
<style>
|
||||
/* Drop overlay styles */
|
||||
.drop-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 4px dashed #3b82f6;
|
||||
z-index: 999;
|
||||
pointer-events: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.drop-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
.drop-message {
|
||||
background-color: white;
|
||||
padding: 2rem 3rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
.drop-message i {
|
||||
font-size: 3rem;
|
||||
color: #3b82f6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.drop-message p {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Upload progress modal */
|
||||
.upload-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
.upload-modal.active {
|
||||
display: block;
|
||||
}
|
||||
.upload-modal-header {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.upload-modal-body {
|
||||
padding: 1rem;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.close-modal-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.close-modal-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
<style>
|
||||
.file-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -241,6 +320,28 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Drop overlay -->
|
||||
<div id="dropOverlay" class="drop-overlay">
|
||||
<div class="drop-message">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
<p>Drop files here to upload</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload progress modal -->
|
||||
<div id="uploadModal" class="upload-modal">
|
||||
<div class="upload-modal-header">
|
||||
<span><i class="fas fa-upload"></i> Uploading Files</span>
|
||||
<button class="close-modal-btn" onclick="closeUploadModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="upload-modal-body">
|
||||
<div id="uploadStatusMessage" class="text-sm text-gray-700 mb-2"></div>
|
||||
<div id="uploadProgressContainer" class="space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h2 class="text-3xl font-bold mb-6">File Records</h2>
|
||||
|
||||
@@ -673,6 +774,76 @@
|
||||
alert(`Error reprocessing files: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== Drag-and-Drop Upload Functionality =====
|
||||
const dropOverlay = document.getElementById('dropOverlay');
|
||||
const uploadModal = document.getElementById('uploadModal');
|
||||
const uploadStatusMessage = document.getElementById('uploadStatusMessage');
|
||||
const uploadProgressContainer = document.getElementById('uploadProgressContainer');
|
||||
|
||||
let dragCounter = 0; // Track nested drag events
|
||||
|
||||
// Show overlay when dragging files over the window
|
||||
window.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter++;
|
||||
|
||||
// Only show overlay if dragging files
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
dropOverlay.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter--;
|
||||
|
||||
if (dragCounter === 0) {
|
||||
dropOverlay.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
});
|
||||
|
||||
window.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dragCounter = 0;
|
||||
dropOverlay.classList.remove('active');
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
// Show upload modal
|
||||
uploadModal.classList.add('active');
|
||||
uploadProgressContainer.innerHTML = '';
|
||||
|
||||
// Process the dropped files
|
||||
processFiles(e.dataTransfer.files, uploadProgressContainer, uploadStatusMessage);
|
||||
|
||||
// Optionally reload page after uploads complete (with a delay)
|
||||
setTimeout(() => {
|
||||
const fileStatuses = document.querySelectorAll('.file-status');
|
||||
let allCompleted = true;
|
||||
fileStatuses.forEach(status => {
|
||||
if (!status.textContent.includes('Success') && !status.textContent.includes('Error')) {
|
||||
allCompleted = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (allCompleted && fileStatuses.length > 0) {
|
||||
// Refresh the page after a short delay to show the new files
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
function closeUploadModal() {
|
||||
uploadModal.classList.remove('active');
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+19
-180
@@ -37,55 +37,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/upload.js"></script>
|
||||
<script>
|
||||
// Configuration
|
||||
const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
|
||||
// Allowed file types
|
||||
const ACCEPTED_TYPES = {
|
||||
// PDF files
|
||||
'application/pdf': true,
|
||||
|
||||
// Image formats
|
||||
'image/jpeg': true, 'image/jpg': true, 'image/png': true,
|
||||
'image/gif': true, 'image/bmp': true, 'image/tiff': true,
|
||||
'image/webp': true, 'image/svg+xml': true,
|
||||
|
||||
// Office document formats - Word
|
||||
'application/msword': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': true,
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': true,
|
||||
'application/vnd.ms-word.document.macroEnabled.12': true,
|
||||
|
||||
// Excel
|
||||
'application/vnd.ms-excel': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true,
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': true,
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12': true,
|
||||
|
||||
// PowerPoint
|
||||
'application/vnd.ms-powerpoint': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template': true,
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow': true,
|
||||
|
||||
// Other common formats
|
||||
'text/plain': true,
|
||||
'text/csv': true,
|
||||
'application/rtf': true,
|
||||
'text/rtf': true,
|
||||
'text/html': true,
|
||||
'application/xml': true,
|
||||
'text/xml': true
|
||||
};
|
||||
|
||||
// File extensions that are always allowed (even if mime type is not recognized)
|
||||
const ACCEPTED_EXTENSIONS = [
|
||||
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
||||
'.odt', '.ods', '.odp', '.rtf', '.txt', '.csv',
|
||||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg', '.md'
|
||||
];
|
||||
|
||||
// Drag-and-drop / file input logic
|
||||
const dropZone = document.getElementById("dropZone");
|
||||
const fileInput = document.getElementById("fileInput");
|
||||
@@ -109,146 +62,32 @@
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove("bg-gray-100");
|
||||
if (e.dataTransfer.files.length) {
|
||||
processFiles(e.dataTransfer.files);
|
||||
// Clear previous upload progress
|
||||
uploadProgress.innerHTML = "";
|
||||
|
||||
// Create progress container
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className = "space-y-2";
|
||||
uploadProgress.appendChild(progressContainer);
|
||||
|
||||
// Use the shared processFiles function
|
||||
processFiles(e.dataTransfer.files, progressContainer, statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(e) {
|
||||
if (e.target.files.length) {
|
||||
processFiles(e.target.files);
|
||||
}
|
||||
}
|
||||
|
||||
function processFiles(files) {
|
||||
if (files.length === 0) return;
|
||||
|
||||
statusMessage.textContent = `Processing ${files.length} file(s)...`;
|
||||
|
||||
// Clear previous upload progress
|
||||
uploadProgress.innerHTML = "";
|
||||
|
||||
// Create progress container
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className = "space-y-2";
|
||||
uploadProgress.appendChild(progressContainer);
|
||||
|
||||
// Process each file
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
validateAndUpload(file, progressContainer);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndUpload(file, progressContainer) {
|
||||
// Create progress element for this file
|
||||
const fileProgress = document.createElement("div");
|
||||
fileProgress.className = "flex flex-col mb-2";
|
||||
fileProgress.innerHTML = `
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm truncate" title="${file.name}">${file.name}</span>
|
||||
<span class="text-xs text-gray-500">${formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 h-2 rounded-full mt-1">
|
||||
<div class="file-progress-bar bg-blue-500 h-2 rounded-full" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="file-status text-xs text-gray-600 mt-1">Validating...</div>
|
||||
`;
|
||||
progressContainer.appendChild(fileProgress);
|
||||
|
||||
const progressBar = fileProgress.querySelector(".file-progress-bar");
|
||||
const statusEl = fileProgress.querySelector(".file-status");
|
||||
|
||||
// Validate file type by checking both MIME type and extension
|
||||
const isValidMimeType = ACCEPTED_TYPES[file.type] || false;
|
||||
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
const isValidExtension = ACCEPTED_EXTENSIONS.includes(fileExtension);
|
||||
|
||||
if (!isValidMimeType && !isValidExtension) {
|
||||
statusEl.textContent = `Error: ${file.name} - Unsupported file type`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
statusEl.textContent = `Error: ${file.name} - File size exceeds 500MB limit`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the file
|
||||
uploadFile(file, progressBar, statusEl);
|
||||
}
|
||||
|
||||
async function uploadFile(file, progressBar, statusEl) {
|
||||
statusEl.textContent = `Uploading...`;
|
||||
try {
|
||||
let formData = new FormData();
|
||||
formData.append("file", file);
|
||||
// Clear previous upload progress
|
||||
uploadProgress.innerHTML = "";
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "/api/ui-upload", true);
|
||||
// Create progress container
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className = "space-y-2";
|
||||
uploadProgress.appendChild(progressContainer);
|
||||
|
||||
xhr.upload.onprogress = (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
progressBar.style.width = percentComplete + "%";
|
||||
statusEl.textContent = `Uploading: ${Math.round(percentComplete)}%`;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
const result = JSON.parse(xhr.responseText);
|
||||
progressBar.style.width = "100%";
|
||||
progressBar.className = "file-progress-bar bg-green-500 h-2 rounded-full";
|
||||
statusEl.textContent = `Success: Task ID: ${result.task_id}`;
|
||||
statusEl.className = "text-xs text-green-600 mt-1";
|
||||
updateOverallStatus();
|
||||
} else {
|
||||
throw new Error(`Upload failed with status ${xhr.status}`);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
throw new Error("Network error occurred");
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
|
||||
} catch (err) {
|
||||
statusEl.textContent = `Error: ${err.message}`;
|
||||
statusEl.className = "text-xs text-red-500 mt-1";
|
||||
progressBar.className = "file-progress-bar bg-red-500 h-2 rounded-full";
|
||||
updateOverallStatus();
|
||||
// Use the shared processFiles function
|
||||
processFiles(e.target.files, progressContainer, statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverallStatus() {
|
||||
// Count success/failure
|
||||
const fileStatuses = document.querySelectorAll('.file-status');
|
||||
let completed = 0;
|
||||
let total = fileStatuses.length;
|
||||
|
||||
fileStatuses.forEach(status => {
|
||||
if (status.textContent.includes('Success') || status.textContent.includes('Error')) {
|
||||
completed++;
|
||||
}
|
||||
});
|
||||
|
||||
if (completed === total) {
|
||||
statusMessage.textContent = `All uploads completed (${completed}/${total})`;
|
||||
} else {
|
||||
statusMessage.textContent = `Uploading files (${completed}/${total})`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user