From b615c0719e846ddaf5693d6d7b56f1e7ca1cfffe Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:43:05 +0530 Subject: [PATCH] Support upload for files via drag/drop in the web UI (#666) * Add additional styling changes for showing UI changes when dragging file to the main screen * Add a loading spinner when file upload is in progress, and don't index github/notion when indexing files * Add an explicit icon for file uploading in the chat button menu * Add appropriate dragover styling when picking a file from the file picker/browser * Add a loading screen when retrieving chat history. Fix width of the chat window. Put attachment icon to the left of chat input --- src/khoj/interface/web/chat.html | 244 ++++++++++++++++++++++++++++++- src/khoj/routers/indexer.py | 58 ++++---- 2 files changed, 268 insertions(+), 34 deletions(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index ca6064e29..13e6da2cc 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -24,6 +24,7 @@ To get started, just start typing below. You can also type / to see a list of commands. `.trim() + const allowedExtensions = ['text/org', 'text/markdown', 'text/plain', 'text/html', 'application/pdf']; let chatOptions = []; function copyProgrammaticOutput(event) { // Remove the first 4 characters which are the "Copy" button @@ -584,11 +585,154 @@ document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; } + function openFileBrowser() { + event.preventDefault(); + var overlayText = document.getElementById("dropzone-overlay"); + var dropzone = document.getElementById('chat-body'); + + if (overlayText == null) { + dropzone.classList.add('dragover'); + var overlayText = document.createElement("div"); + overlayText.innerHTML = "Select a file to share it with Khoj"; + overlayText.className = "dropzone-overlay"; + overlayText.id = "dropzone-overlay"; + dropzone.appendChild(overlayText); + } + + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.addEventListener('change', function() { + const selectedFile = fileInput.files[0]; + uploadDataForIndexing(selectedFile); + }); + + // Remove overlay text after file input is closed + fileInput.addEventListener('blur', function() { + dropzone.classList.remove('dragover'); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + }); + + // Remove overlay text if file input is cancelled + fileInput.addEventListener('cancel', function() { + dropzone.classList.remove('dragover'); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + }); + + fileInput.click(); + } + + function uploadDataForIndexing(file) { + if (!allowedExtensions.includes(file.type)) { + alert("Sorry, that file type is not yet supported"); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + return; + } + + const fileName = file.name; + var fileContents = null; + + var reader = new FileReader(); + const formData = new FormData(); + + var dropzone = document.getElementById('chat-body'); + + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + // Display loading spinner + var loadingSpinner = document.createElement("div"); + overlayText.innerHTML = "Uploading file for indexing"; + loadingSpinner.className = "spinner"; + overlayText.appendChild(loadingSpinner); + } + + reader.onload = function (event) { + fileContents = event.target.result; + let fileObj = new Blob([fileContents], { type: file.type }); + formData.append("files", fileObj, file.name); + console.log(formData); + + fetch("/api/v1/index/update?force=false&client=web", { + method: "POST", + body: formData, + }) + .then((data) => { + console.log(data); + dropzone.classList.remove('dragover'); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + // Display indexing success message + flashStatusInChatInput("✅ File indexed successfully"); + + + }) + .catch((error) => { + console.log(error); + dropzone.classList.remove('dragover'); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + // Display indexing failure message + flashStatusInChatInput("⛔️ Failed to upload file for indexing"); + }); + }; + + reader.readAsArrayBuffer(file); + } + + function setupDropZone() { + var dropzone = document.getElementById('chat-body'); + + dropzone.ondragover = function(event) { + event.preventDefault(); + this.classList.add('dragover'); + var overlayText = document.getElementById("dropzone-overlay"); + console.log("ondragover triggered"); + + if (overlayText == null) { + var overlayText = document.createElement("div"); + overlayText.innerHTML = "Drop file to share it with Khoj"; + overlayText.className = "dropzone-overlay"; + overlayText.id = "dropzone-overlay"; + this.appendChild(overlayText); + } + }; + + dropzone.ondragleave = function(event) { + event.preventDefault(); + this.classList.remove('dragover'); + console.log("ondragleave triggered"); + var overlayText = document.getElementById("dropzone-overlay"); + if (overlayText != null) { + overlayText.remove(); + } + }; + + dropzone.ondrop = function(event) { + event.preventDefault(); + + var file = event.dataTransfer.files[0]; + uploadDataForIndexing(file); + }; + } + window.onload = loadChat; function loadChat() { let chatBody = document.getElementById("chat-body"); chatBody.innerHTML = ""; + chatBody.classList.add("relative-position"); let conversationId = chatBody.dataset.conversationId; let chatHistoryUrl = `/api/chat/history?client=web`; if (conversationId) { @@ -599,6 +743,11 @@ handleCollapseSidePanel(); } + // Create loading screen and add it to chat-body + let loadingScreen = document.createElement('div'); + loadingScreen.classList.add('loading-screen', 'gradient-animation'); + chatBody.appendChild(loadingScreen); + fetch(chatHistoryUrl, { method: "GET" }) .then(response => response.json()) .then(data => { @@ -625,6 +774,7 @@ chatBody.dataset.conversationId = conversationId; chatBody.dataset.conversationTitle = conversationTitle; + let chatBodyWrapper = document.getElementById("chat-body-wrapper"); const fullChatLog = response.chat || []; fullChatLog.forEach(chat_log => { @@ -638,13 +788,20 @@ chat_log.intent?.type, chat_log.intent?.["inferred-queries"]); } - + loadingScreen.style.height = chatBody.scrollHeight + 'px'; }); - let chatBodyWrapper = document.getElementById("chat-body-wrapper"); + // Add fade out animation to loading screen and remove it after the animation ends + loadingScreen.classList.remove('gradient-animation'); + loadingScreen.classList.add('fade-out-animation'); chatBodyWrapperHeight = chatBodyWrapper.clientHeight; - chatBody.style.height = chatBodyWrapperHeight; + setTimeout(() => { + loadingScreen.remove(); + chatBody.classList.remove("relative-position"); + setupDropZone(); + }, 500); + }) .catch(err => { console.log(err); @@ -1043,6 +1200,11 @@