From a7128cecd250c69b67d117ba84da5632156e9eef Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Feb 2026 06:05:35 +0000
Subject: [PATCH] Add drag-and-drop upload functionality to Files view
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
frontend/static/js/upload.js | 264 +++++++++++++++++++++++++++++++++
frontend/templates/files.html | 171 +++++++++++++++++++++
frontend/templates/upload.html | 199 +++----------------------
3 files changed, 454 insertions(+), 180 deletions(-)
create mode 100644 frontend/static/js/upload.js
diff --git a/frontend/static/js/upload.js b/frontend/static/js/upload.js
new file mode 100644
index 00000000..48be819c
--- /dev/null
+++ b/frontend/static/js/upload.js
@@ -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 = `
+
+ ${file.name}
+ ${formatFileSize(file.size)}
+
+
+ Validating...
+ `;
+
+ 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);
+ }
+ });
+}
diff --git a/frontend/templates/files.html b/frontend/templates/files.html
index 6d6d0560..16909cc7 100644
--- a/frontend/templates/files.html
+++ b/frontend/templates/files.html
@@ -3,7 +3,86 @@
{% block head_extra %}
+