From 44f8f20ea7b24b47e6f921d1aef2abae6bdc0f78 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:55:35 -0800 Subject: [PATCH] Miscellaneous bugs and fixes for chat sessions (#646) * Display given_name field only if it is not None * Add default slugs in the migration script * Ensure that updated_at is saved appropriately, make sure most recent chat is returned for default history * Remove the bin button from the chat interface, given deletion is handled in the drop-down menus * Refresh the side panel when a new chat is created * Improveme tool retrieval prompt, don't let /online fail, and improve parsing of extract questions * Fix ending chat response by offline chat on hitting a stop phrase Previously the whole phrase wouldn't be in the same response chunk, so chat response wouldn't stop on hitting a stop phrase Now use a queue to keep track of last 3 chunks, and to stop responding when hit a stop phrase * Make chat on Obsidian backward compatible post chat session API updates - Make chat on Obsidian get chat history from `responseJson.response.chat' when available (i.e when using new api) - Else fallback to loading chat history from responseJson.response (i.e when using old api) * Fix detecting success of indexing update in khoj.el When khoj.el attempts to index on a Khoj server served behind an https endpoint, the success reponse status contains plist with certs. This doesn't mean the update failed. Look for :errors key in status instead to determine if indexing API call failed. This fixes detecting indexing API call success on the Khoj Emacs client, even for Khoj servers running behind SSL/HTTPS * Fix the mechanism for populating notes references in the conversation primer for both offline and online chat * Return conversation.default when empty list for dynamic prompt selection, send all cmds in telemetry * Fix making chat on Obsidian backward compatible post chat session API updates New API always has conversation_id set, not `chat' which can be unset when chat session is empty. So use conversation_id to decide whether to get chat logs from `responseJson.response.chat' or `responseJson.response' instead --------- Co-authored-by: Debanjum Singh Solanky --- src/interface/desktop/assets/khoj.css | 12 +- src/interface/desktop/chat.html | 210 ++++++------- src/interface/desktop/main.js | 2 +- src/interface/emacs/khoj.el | 2 +- src/interface/obsidian/src/chat_modal.ts | 2 +- src/khoj/database/adapters/__init__.py | 4 +- .../0030_conversation_slug_and_title.py | 16 + src/khoj/interface/web/chat.html | 293 +++++++++--------- src/khoj/interface/web/config.html | 2 +- .../conversation/offline/chat_model.py | 24 +- src/khoj/processor/conversation/openai/gpt.py | 31 +- src/khoj/processor/conversation/prompts.py | 10 +- src/khoj/processor/tools/online_search.py | 3 +- src/khoj/routers/api.py | 5 +- src/khoj/routers/api_chat.py | 3 +- src/khoj/routers/helpers.py | 4 + src/khoj/utils/helpers.py | 4 +- tests/test_gpt4all_chat_director.py | 27 +- tests/test_openai_chat_director.py | 25 +- 19 files changed, 348 insertions(+), 331 deletions(-) diff --git a/src/interface/desktop/assets/khoj.css b/src/interface/desktop/assets/khoj.css index 83bde750..b8157bcc 100644 --- a/src/interface/desktop/assets/khoj.css +++ b/src/interface/desktop/assets/khoj.css @@ -65,12 +65,18 @@ font-weight: 300; } -.khoj-header { +div.khoj-header { display: grid; grid-auto-flow: column; gap: 20px; - padding: 16px 0; + padding: 24px 16px 0px 0px; margin: 0 0 16px 0; + -webkit-user-select: none; + -webkit-app-region: drag; +} + +a.khoj-nav { + -webkit-app-region: no-drag; } nav.khoj-nav { @@ -117,7 +123,7 @@ img.khoj-logo { display: grid; grid-auto-flow: column; gap: 20px; - padding: 12px 10px; + padding: 24px 10px 10px 10px; margin: 0 0 16px 0; } diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index d0153864..6d5ced15 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -349,6 +349,7 @@ let data = await response.json(); conversationID = data.conversation_id; chat_body.dataset.conversationId = conversationID; + await refreshChatSessionsPanel(); } @@ -665,6 +666,107 @@ return; }); + await refreshChatSessionsPanel(); + + fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers }) + .then(response => response.json()) + .then(data => { + // Render chat options, if any + if (data.length > 0) { + let questionStarterSuggestions = document.getElementById("question-starters"); + for (let index in data) { + let questionStarter = data[index]; + let questionStarterButton = document.createElement('button'); + questionStarterButton.innerHTML = questionStarter; + questionStarterButton.classList.add("question-starter"); + questionStarterButton.addEventListener('click', function() { + questionStarterSuggestions.style.display = "none"; + document.getElementById("chat-input").value = questionStarter; + chat(); + }); + questionStarterSuggestions.appendChild(questionStarterButton); + } + questionStarterSuggestions.style.display = "grid"; + } + }) + .catch(err => { + return; + }); + + fetch(`${hostURL}/api/chat/options`, { headers }) + .then(response => response.json()) + .then(data => { + // Render chat options, if any + if (data) { + chatOptions = data; + } + }) + .catch(err => { + return; + }); + + // Fill query field with value passed in URL query parameters, if any. + var query_via_url = new URLSearchParams(window.location.search).get("q"); + if (query_via_url) { + document.getElementById("chat-input").value = query_via_url; + chat(); + } + } + + function flashStatusInChatInput(message) { + // Get chat input element and original placeholder + let chatInput = document.getElementById("chat-input"); + let originalPlaceholder = chatInput.placeholder; + // Set placeholder to message + chatInput.placeholder = message; + // Reset placeholder after 2 seconds + setTimeout(() => { + chatInput.placeholder = originalPlaceholder; + }, 2000); + } + + function createNewConversation() { + let chatBody = document.getElementById("chat-body"); + chatBody.innerHTML = ""; + flashStatusInChatInput("📝 New conversation started"); + chatBody.dataset.conversationId = ""; + chatBody.dataset.conversationTitle = ""; + renderMessage("Hey 👋🏾, what's up?", "khoj"); + } + + async function clearConversationHistory() { + let chatInput = document.getElementById("chat-input"); + let originalPlaceholder = chatInput.placeholder; + let chatBody = document.getElementById("chat-body"); + let conversationId = chatBody.dataset.conversationId; + + let deleteURL = `/api/chat/history?client=desktop`; + if (conversationId) { + deleteURL += `&conversation_id=${conversationId}`; + } + + const hostURL = await window.hostURLAPI.getURL(); + const khojToken = await window.tokenAPI.getToken(); + const headers = { 'Authorization': `Bearer ${khojToken}` }; + + fetch(`${hostURL}${deleteURL}`, { method: "DELETE", headers }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(data => { + chatBody.innerHTML = ""; + chatBody.dataset.conversationId = ""; + chatBody.dataset.conversationTitle = ""; + loadChat(); + flashStatusInChatInput("🗑 Cleared conversation history"); + }) + .catch(err => { + flashStatusInChatInput("⛔️ Failed to clear conversation history"); + }) + } + + async function refreshChatSessionsPanel() { + const hostURL = await window.hostURLAPI.getURL(); + const khojToken = await window.tokenAPI.getToken(); + const headers = { 'Authorization': `Bearer ${khojToken}` }; fetch(`${hostURL}/api/chat/sessions`, { method: "GET", headers }) .then(response => response.json()) @@ -799,101 +901,9 @@ conversationListBody.appendChild(conversationButton); } } - }) - - fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers }) - .then(response => response.json()) - .then(data => { - // Render chat options, if any - if (data.length > 0) { - let questionStarterSuggestions = document.getElementById("question-starters"); - for (let index in data) { - let questionStarter = data[index]; - let questionStarterButton = document.createElement('button'); - questionStarterButton.innerHTML = questionStarter; - questionStarterButton.classList.add("question-starter"); - questionStarterButton.addEventListener('click', function() { - questionStarterSuggestions.style.display = "none"; - document.getElementById("chat-input").value = questionStarter; - chat(); - }); - questionStarterSuggestions.appendChild(questionStarterButton); - } - questionStarterSuggestions.style.display = "grid"; - } - }) - .catch(err => { + }).catch(err => { return; - }); - - fetch(`${hostURL}/api/chat/options`, { headers }) - .then(response => response.json()) - .then(data => { - // Render chat options, if any - if (data) { - chatOptions = data; - } - }) - .catch(err => { - return; - }); - - // Fill query field with value passed in URL query parameters, if any. - var query_via_url = new URLSearchParams(window.location.search).get("q"); - if (query_via_url) { - document.getElementById("chat-input").value = query_via_url; - chat(); - } - } - - function flashStatusInChatInput(message) { - // Get chat input element and original placeholder - let chatInput = document.getElementById("chat-input"); - let originalPlaceholder = chatInput.placeholder; - // Set placeholder to message - chatInput.placeholder = message; - // Reset placeholder after 2 seconds - setTimeout(() => { - chatInput.placeholder = originalPlaceholder; - }, 2000); - } - - function createNewConversation() { - let chatBody = document.getElementById("chat-body"); - chatBody.innerHTML = ""; - flashStatusInChatInput("📝 New conversation started"); - chatBody.dataset.conversationId = ""; - chatBody.dataset.conversationTitle = ""; - renderMessage("Hey 👋🏾, what's up?", "khoj"); - } - - async function clearConversationHistory() { - let chatInput = document.getElementById("chat-input"); - let originalPlaceholder = chatInput.placeholder; - let chatBody = document.getElementById("chat-body"); - let conversationId = chatBody.dataset.conversationId; - - let deleteURL = `/api/chat/history?client=desktop`; - if (conversationId) { - deleteURL += `&conversation_id=${conversationId}`; - } - - const hostURL = await window.hostURLAPI.getURL(); - const khojToken = await window.tokenAPI.getToken(); - const headers = { 'Authorization': `Bearer ${khojToken}` }; - - fetch(`${hostURL}${deleteURL}`, { method: "DELETE", headers }) - .then(response => response.ok ? response.json() : Promise.reject(response)) - .then(data => { - chatBody.innerHTML = ""; - chatBody.dataset.conversationId = ""; - chatBody.dataset.conversationTitle = ""; - loadChat(); - flashStatusInChatInput("🗑 Cleared conversation history"); - }) - .catch(err => { - flashStatusInChatInput("⛔️ Failed to clear conversation history"); - }) + }); } let sendMessageTimeout; @@ -1065,16 +1075,6 @@