diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index f24990e8..5b9d1375 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -140,6 +140,9 @@ // Scroll to bottom of chat-body element chatBody.scrollTop = chatBody.scrollHeight; + + let chatBodyWrapper = document.getElementById("chat-body-wrapper"); + chatBodyWrapperHeight = chatBodyWrapper.clientHeight; } function processOnlineReferences(referenceSection, onlineContext) { @@ -319,14 +322,25 @@ autoResize(); document.getElementById("chat-input").setAttribute("disabled", "disabled"); + let chat_body = document.getElementById("chat-body"); + + let conversationID = chat_body.dataset.conversationId; + let hostURL = await window.hostURLAPI.getURL(); + if (!conversationID) { + let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST" }); + let data = await response.json(); + conversationID = data.conversation_id; + chat_body.dataset.conversationId = conversationID; + } + + // Generate backend API URL to execute query - let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true`; + let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}`; const khojToken = await window.tokenAPI.getToken(); const headers = { 'Authorization': `Bearer ${khojToken}` }; - let chat_body = document.getElementById("chat-body"); let new_response = document.createElement("div"); new_response.classList.add("chat-message", "khoj"); new_response.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date()); @@ -559,7 +573,14 @@ const khojToken = await window.tokenAPI.getToken(); const headers = { 'Authorization': `Bearer ${khojToken}` }; - fetch(`${hostURL}/api/chat/history?client=desktop`, { headers }) + let chatBody = document.getElementById("chat-body"); + let conversationId = chatBody.dataset.conversationId; + let chatHistoryUrl = `/api/chat/history?client=desktop`; + if (conversationId) { + chatHistoryUrl += `&conversation_id=${conversationId}`; + } + + fetch(`${hostURL}${chatHistoryUrl}`, { headers }) .then(response => response.json()) .then(data => { if (data.detail) { @@ -584,17 +605,186 @@ return data.response; }) .then(response => { + conversationId = response.conversation_id; + const conversationTitle = response.slug || `New conversation 🌱`; + + let chatBody = document.getElementById("chat-body"); + chatBody.dataset.conversationId = conversationId; + chatBody.dataset.conversationTitle = conversationTitle; + + const fullChatLog = response.chat || []; - const fullChatLog = response.chat; - // Render conversation history, if any fullChatLog.forEach(chat_log => { - renderMessageWithReference(chat_log.message, chat_log.by, chat_log.context, new Date(chat_log.created), chat_log.onlineContext, chat_log.intent?.type, chat_log.intent?.["inferred-queries"]); - }); + if (chat_log.message != null) { + renderMessageWithReference( + chat_log.message, + chat_log.by, + chat_log.context, + new Date(chat_log.created), + chat_log.onlineContext, + chat_log.intent?.type, + chat_log.intent?.["inferred-queries"]); + } + }) + let chatBodyWrapper = document.getElementById("chat-body-wrapper"); + chatBodyWrapperHeight = chatBodyWrapper.clientHeight; + + chatBody.style.height = chatBodyWrapperHeight; }) .catch(err => { + // If the server returns a 500 error with detail, render a setup hint. + first_run_message = `Hi 👋🏾, to get started: +
    +
  1. Generate an API token in the Khoj Web settings
  2. +
  3. Paste it into the API Key field in the Khoj Desktop settings
  4. +
` + .trim() + .replace(/(\r\n|\n|\r)/gm, ""); + + renderMessage(first_run_message, "khoj", null, null, true); + + // Disable chat input field and update placeholder text + document.getElementById("chat-input").setAttribute("disabled", "disabled"); + document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat"); return; }); + + fetch(`${hostURL}/api/chat/sessions`, { method: "GET", headers }) + .then(response => response.json()) + .then(data => { + let conversationListBody = document.getElementById("conversation-list-body"); + conversationListBody.innerHTML = ""; + let conversationListBodyHeader = document.getElementById("conversation-list-header"); + + let chatBody = document.getElementById("chat-body"); + conversationId = chatBody.dataset.conversationId; + + if (data.length > 0) { + conversationListBodyHeader.style.display = "block"; + for (let index in data) { + let conversation = data[index]; + let conversationButton = document.createElement('div'); + let incomingConversationId = conversation["conversation_id"]; + const conversationTitle = conversation["slug"] || `New conversation 🌱`; + conversationButton.innerHTML = conversationTitle; + conversationButton.classList.add("conversation-button"); + if (incomingConversationId == conversationId) { + conversationButton.classList.add("selected-conversation"); + } + conversationButton.addEventListener('click', function() { + let chatBody = document.getElementById("chat-body"); + chatBody.innerHTML = ""; + chatBody.dataset.conversationId = incomingConversationId; + chatBody.dataset.conversationTitle = conversationTitle; + loadChat(); + }); + let threeDotMenu = document.createElement('div'); + threeDotMenu.classList.add("three-dot-menu"); + let threeDotMenuButton = document.createElement('button'); + threeDotMenuButton.innerHTML = "⋮"; + threeDotMenuButton.classList.add("three-dot-menu-button"); + threeDotMenuButton.addEventListener('click', function(event) { + event.stopPropagation(); + + let existingChildren = threeDotMenu.children; + + if (existingChildren.length > 1) { + // Skip deleting the first, since that's the menu button. + for (let i = 1; i < existingChildren.length; i++) { + existingChildren[i].remove(); + } + return; + } + + let conversationMenu = document.createElement('div'); + conversationMenu.classList.add("conversation-menu"); + + let deleteButton = document.createElement('button'); + deleteButton.innerHTML = "Delete"; + deleteButton.classList.add("delete-conversation-button"); + deleteButton.classList.add("three-dot-menu-button-item"); + deleteButton.addEventListener('click', function() { + let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`; + fetch(`${hostURL}${deleteURL}` , { method: "DELETE", headers }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(data => { + let chatBody = document.getElementById("chat-body"); + chatBody.innerHTML = ""; + chatBody.dataset.conversationId = ""; + chatBody.dataset.conversationTitle = ""; + loadChat(); + }) + .catch(err => { + return; + }); + }); + conversationMenu.appendChild(deleteButton); + threeDotMenu.appendChild(conversationMenu); + + let editTitleButton = document.createElement('button'); + editTitleButton.innerHTML = "Rename"; + editTitleButton.classList.add("edit-title-button"); + editTitleButton.classList.add("three-dot-menu-button-item"); + editTitleButton.addEventListener('click', function(event) { + event.stopPropagation(); + + let conversationMenuChildren = conversationMenu.children; + + let totalItems = conversationMenuChildren.length; + + for (let i = totalItems - 1; i >= 0; i--) { + conversationMenuChildren[i].remove(); + } + + // Create a dialog box to get new title for conversation + let conversationTitleInputBox = document.createElement('div'); + conversationTitleInputBox.classList.add("conversation-title-input-box"); + let conversationTitleInput = document.createElement('input'); + conversationTitleInput.classList.add("conversation-title-input"); + + conversationTitleInput.value = conversationTitle; + + conversationTitleInput.addEventListener('click', function(event) { + event.stopPropagation(); + if (event.key === "Enter") { + event.preventDefault(); + conversationTitleInputButton.click(); + } + }); + + conversationTitleInputBox.appendChild(conversationTitleInput); + let conversationTitleInputButton = document.createElement('button'); + conversationTitleInputButton.innerHTML = "Save"; + conversationTitleInputButton.classList.add("three-dot-menu-button-item"); + conversationTitleInputButton.addEventListener('click', function(event) { + event.stopPropagation(); + let newTitle = conversationTitleInput.value; + if (newTitle != null) { + let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`; + fetch(`${hostURL}${editURL}` , { method: "PATCH" }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(data => { + conversationButton.innerHTML = newTitle; + }) + .catch(err => { + return; + }); + conversationTitleInputBox.remove(); + }}); + conversationTitleInputBox.appendChild(conversationTitleInputButton); + conversationMenu.appendChild(conversationTitleInputBox); + }); + conversationMenu.appendChild(editTitleButton); + threeDotMenu.appendChild(conversationMenu); + }); + threeDotMenu.appendChild(threeDotMenuButton); + conversationButton.appendChild(threeDotMenu); + conversationListBody.appendChild(conversationButton); + } + } + }) + fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers }) .then(response => response.json()) .then(data => { @@ -652,19 +842,36 @@ }, 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}/api/chat/history?client=desktop`, { method: "DELETE", headers }) + 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"); }) @@ -776,6 +983,14 @@ // Stop the countdown timer UI document.getElementById('countdown-circle').style.animation = "none"; }; + + function handleCollapseSidePanel() { + document.getElementById('side-panel').classList.toggle('collapsed'); + document.getElementById('new-conversation').classList.toggle('collapsed'); + document.getElementById('existing-conversations').classList.toggle('collapsed'); + + document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly'); + }
@@ -793,47 +1008,80 @@
- -
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
- - + + - - @@ -855,15 +1103,86 @@ font-weight: 300; line-height: 1.5em; } + body > * { padding: 10px; margin: 10px; } + + input.conversation-title-input { + font-family: var(--font-family); + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + padding: 5px; + border: 1px solid var(--main-text-color); + border-radius: 5px; + margin: 4px; + } + + input.conversation-title-input:focus { + outline: none; + } + + #chat-section-wrapper { + display: grid; + grid-template-columns: auto auto; + grid-column-gap: 10px; + grid-row-gap: 10px; + padding: 10px; + margin: 10px; + overflow-y: scroll; + } + + #chat-section-wrapper.mobile-friendly { + grid-template-columns: auto auto; + } + + #chat-body-wrapper { + display: flex; + flex-direction: column; + overflow: hidden; + } + + #side-panel { + padding: 10px; + background: var(--background-color); + border-radius: 5px; + box-shadow: 0 0 11px #aaa; + overflow-y: scroll; + text-align: left; + transition: width 0.3s ease-in-out; + width: 250px; + } + + div#side-panel.collapsed { + width: 1px; + display: block; + overflow: hidden; + } + + div#collapse-side-panel { + align-self: center; + padding: 8px; + } + + div#conversation-list-body { + display: grid; + grid-template-columns: 1fr; + grid-gap: 8px; + } + + div#side-panel-wrapper { + display: flex + } + + #chat-body { font-size: small; margin: 0px; line-height: 20px; - overflow-y: scroll; /* Make chat body scroll to see history */ + overflow-y: scroll; + overflow-x: hidden; } /* add chat metatdata to bottom of bubble */ .chat-message::after { @@ -871,7 +1190,7 @@ display: block; font-size: x-small; color: #475569; - margin: -8px 4px 0 -5px; + margin: -8px 4px 0px 0px; } /* move message by khoj to left */ .chat-message.khoj { @@ -962,6 +1281,7 @@ grid-row-gap: 10px; background: #f9fafc; align-items: center; + background-color: var(--background-color); } .option:hover { box-shadow: 0 0 11px #aaa; @@ -998,9 +1318,33 @@ margin-top: -2px; margin-left: -5px; } + + .side-panel-button { + background: var(--background-color); + border: none; + box-shadow: none; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.3s ease-in-out; + border-radius: 5%;; + font-family: var(--font-family); + padding: 8px; + font-size: large; + } + + svg.side-panel-collapse { + width: 30px; + height: 30px; + } + + .side-panel-button:hover, .input-row-button:hover { background: var(--primary-hover); } + + .side-panel-button:active, .input-row-button:active { background: var(--primary-active); } @@ -1236,10 +1580,36 @@ #clear-chat-button { margin-left: 0; } + + div#side-panel.collapsed { + width: 0px; + display: block; + overflow: hidden; + padding: 0; + } + + svg.side-panel-collapse { + width: 24px; + height: 24px; + } + + #chat-body-wrapper { + min-width: 0; + } + + div#chat-section-wrapper { + padding: 4px; + margin: 4px; + grid-column-gap: 4px; + } + div#collapse-side-panel { + align-self: center; + padding: 0px; + } } @media only screen and (min-width: 600px) { body { - grid-template-columns: auto min(70vw, 100%) auto; + grid-template-columns: auto min(90vw, 100%) auto; grid-template-rows: auto auto minmax(80px, 100%) auto; } body > * { @@ -1252,6 +1622,110 @@ font-size: medium; } + svg.new-convo-button { + width: 20px; + margin-left: 5px; + } + + div#new-conversation { + text-align: left; + border-bottom: 1px solid var(--main-text-color); + margin-bottom: 8px; + } + + button#new-conversation-button { + display: inline-flex; + align-items: center; + } + + div.conversation-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.2s ease-in-out; + text-align: left; + display: flex; + position: relative; + } + + .three-dot-menu { + display: none; + /* background: var(--background-color); */ + /* border: 1px solid var(--main-text-color); */ + border-radius: 5px; + /* position: relative; */ + position: absolute; + right: 4; + top: 4; + } + + button.three-dot-menu-button-item { + background: var(--background-color); + color: var(--main-text-color); + border: none; + box-shadow: none; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.3s ease-in-out; + font-family: var(--font-family); + border-radius: 4px; + right: 0; + } + + button.three-dot-menu-button-item:hover { + background: var(--primary-hover); + color: var(--primary-inverse); + } + + .three-dot-menu-button { + background: var(--background-color); + border: none; + box-shadow: none; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.3s ease-in-out; + font-family: var(--font-family); + border-radius: 4px; + right: 0; + } + + .conversation-button:hover .three-dot-menu { + display: block; + } + + div.conversation-menu { + position: absolute; + z-index: 1; + top: 100%; + right: 0; + text-align: right; + background-color: var(--background-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + box-shadow: 0 0 11px #aaa; + } + + div.conversation-button:hover { + background: var(--primary-hover); + color: var(--primary-inverse); + } + + div.selected-conversation { + background: var(--primary-hover) !important; + color: var(--primary-inverse) !important; + } + @keyframes gradient { 0% { background-position: 0% 50%; diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index a98b15c8..c4839956 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -225,7 +225,8 @@ function pushDataToKhoj (regenerate = false) { .finally(() => { // Syncing complete syncing = false; - if (win = BrowserWindow.getAllWindows()[0]) { + const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')); + if (win) { win.webContents.send('update-state', state); } }); diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index 676776b9..296afec2 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -10,7 +10,7 @@ "main": "main.js", "private": false, "devDependencies": { - "electron": "25.8.4" + "electron": "28.2.1" }, "scripts": { "start": "yarn electron ." diff --git a/src/interface/desktop/yarn.lock b/src/interface/desktop/yarn.lock index 12929646..539bfa97 100644 --- a/src/interface/desktop/yarn.lock +++ b/src/interface/desktop/yarn.lock @@ -379,10 +379,10 @@ electron-updater@^4.6.1: lodash.isequal "^4.5.0" semver "^7.3.5" -electron@25.8.4: - version "25.8.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-25.8.4.tgz#b50877aac7d96323920437baf309ad86382cb455" - integrity sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg== +electron@28.2.1: + version "28.2.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-28.2.1.tgz#8edf2be24d97160b7eb52b7ce9a2424cf14c0791" + integrity sha512-wlzXf+OvOiVlBf9dcSeMMf7Q+N6DG+wtgFbMK0sA/JpIJcdosRbLMQwLg/LTwNVKIbmayqFLDp4FmmFkEMhbYA== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18"