From ea85ebdacb8b73daa1c1c5368f9ef8cf21b4b84f Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 19 Jan 2024 12:22:59 +0530 Subject: [PATCH 01/11] Add and improve chat input pane, actions, icons on web client - Use SVG icons in chat footer on web - Move delete icon to left of chat input. This makes it harder to inadvertently click - Add send button to chat footer. Enter being the only way to send messages is not intuitive, outside standard modern UI patterns - Color chat message send button to make it primary CTA on web client - Make chat footer shorter. Use no or round border on action buttons --- src/khoj/interface/web/chat.html | 72 ++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 2a813779..54dc9692 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -528,7 +528,7 @@ To get started, just start typing below. You can also type / to see a list of co const textarea = document.getElementById('chat-input'); const scrollTop = textarea.scrollTop; textarea.style.height = '0'; - const scrollHeight = textarea.scrollHeight; + const scrollHeight = textarea.scrollHeight + 8; // +8 accounts for padding textarea.style.height = Math.min(scrollHeight, 200) + 'px'; textarea.scrollTop = scrollTop; document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; @@ -642,6 +642,7 @@ To get started, just start typing below. You can also type / to see a list of co let mediaRecorder; function speechToText() { const speakButtonImg = document.getElementById('speak-button-img'); + const stopRecordButtonImg = document.getElementById('stop-record-button-img'); const chatInput = document.getElementById('chat-input'); const sendToServer = (audioBlob) => { @@ -679,8 +680,8 @@ To get started, just start typing below. You can also type / to see a list of co }); mediaRecorder.start(); - speakButtonImg.src = '/static/assets/icons/stop-solid.svg'; - speakButtonImg.alt = 'Stop Transcription'; + speakButtonImg.style.display = 'none'; + stopRecordButtonImg.style.display = 'initial'; }; // Toggle recording @@ -695,8 +696,8 @@ To get started, just start typing below. You can also type / to see a list of co mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(track => track.stop()); mediaRecorder = null; - speakButtonImg.src = '/static/assets/icons/microphone-solid.svg'; - speakButtonImg.alt = 'Transcribe'; + speakButtonImg.style.display = 'initial'; + stopRecordButtonImg.style.display = 'none'; } } @@ -718,12 +719,31 @@ To get started, just start typing below. You can also type / to see a list of co @@ -963,7 +983,7 @@ To get started, just start typing below. You can also type / to see a list of co } #input-row { display: grid; - grid-template-columns: auto 32px 32px; + grid-template-columns: 32px auto 32px 40px; grid-column-gap: 10px; grid-row-gap: 10px; background: var(--background-color); @@ -974,12 +994,13 @@ To get started, just start typing below. You can also type / to see a list of co #chat-input { font-family: roboto, karma, segoe ui, sans-serif; font-size: medium; - height: 54px; + height: 36px; + border-radius: 16px; resize: none; overflow-y: hidden; max-height: 200px; box-sizing: border-box; - padding: 15px; + padding: 4px 0 0 12px; line-height: 1.5em; margin: 0; } @@ -988,15 +1009,18 @@ To get started, just start typing below. You can also type / to see a list of co } .input-row-button { background: var(--background-color); - border: 1px solid var(--main-text-color); - box-shadow: 0 0 11px #aaa; - border-radius: 5px; - padding: 0px; + border: none; + box-shadow: none; + border-radius: 50%; font-size: 14px; font-weight: 300; line-height: 1.5em; cursor: pointer; transition: background 0.3s ease-in-out; + width: 40px; + height: 40px; + margin-top: -2px; + margin-left: -5px; } .input-row-button:hover { background: var(--primary-hover); @@ -1006,6 +1030,17 @@ To get started, just start typing below. You can also type / to see a list of co } .input-row-button-img { width: 24px; + height: 24px; + } + #send-button { + padding-top: 0; + padding-right: 3px; + } + #send-button-img { + width: 28px; + height: 28px; + background: var(--primary-hover); + border-radius: 50%; } @@ -1083,6 +1118,9 @@ To get started, just start typing below. You can also type / to see a list of co img.text-to-image { max-width: 100%; } + #clear-chat-button { + margin-left: 0; + } } @media only screen and (min-width: 700px) { body { From c0ad64d9a3bf62c2c23a79df7161476c492a00b5 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 19 Jan 2024 20:40:04 +0530 Subject: [PATCH 02/11] Add and improve chat input pane, actions, icons on desktop client - Use SVG icons in chat footer on web - Move delete icon to left of chat input. This makes it harder to inadvertently click - Add send button to chat footer. Enter being the only way to send messages is not intuitive, outside standard modern UI patterns - Color chat message send button to make it primary CTA on web client - Make chat footer shorter. Use no or round border on action buttons --- src/interface/desktop/chat.html | 73 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 7cd75f01..ec5de0c7 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -546,7 +546,7 @@ const textarea = document.getElementById('chat-input'); const scrollTop = textarea.scrollTop; textarea.style.height = '0'; - const scrollHeight = textarea.scrollHeight; + const scrollHeight = textarea.scrollHeight + 8; // +8 accounts for padding textarea.style.height = Math.min(scrollHeight, 200) + 'px'; textarea.scrollTop = scrollTop; document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; @@ -676,6 +676,7 @@ let mediaRecorder; async function speechToText() { const speakButtonImg = document.getElementById('speak-button-img'); + const stopRecordButtonImg = document.getElementById('stop-record-button-img'); const chatInput = document.getElementById('chat-input'); const hostURL = await window.hostURLAPI.getURL(); @@ -716,8 +717,8 @@ }); mediaRecorder.start(); - speakButtonImg.src = './assets/icons/stop-solid.svg'; - speakButtonImg.alt = 'Stop Transcription'; + speakButtonImg.style.display = 'none'; + stopRecordButtonImg.style.display = 'initial'; }; // Toggle recording @@ -732,8 +733,8 @@ mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(track => track.stop()); mediaRecorder = null; - speakButtonImg.src = './assets/icons/microphone-solid.svg'; - speakButtonImg.alt = 'Transcribe'; + speakButtonImg.style.display = 'initial'; + stopRecordButtonImg.style.display = 'none'; } } @@ -764,12 +765,31 @@ @@ -894,7 +914,7 @@ } #input-row { display: grid; - grid-template-columns: auto 32px 32px; + grid-template-columns: 32px auto 32px 40px; grid-column-gap: 10px; grid-row-gap: 10px; background: #f9fafc @@ -905,12 +925,13 @@ #chat-input { font-family: roboto, karma, segoe ui, sans-serif; font-size: small; - height: 54px; + height: 36px; + border-radius: 16px; resize: none; overflow-y: hidden; max-height: 200px; box-sizing: border-box; - padding: 15px; + padding: 7px 0 0 12px; line-height: 1.5em; margin: 0; } @@ -919,15 +940,19 @@ } .input-row-button { background: var(--background-color); - border: 1px solid var(--main-text-color); - box-shadow: 0 0 11px #aaa; - border-radius: 5px; + border: none; + box-shadow: none; + border-radius: 50%; font-size: 14px; font-weight: 300; padding: 0; line-height: 1.5em; cursor: pointer; transition: background 0.3s ease-in-out; + width: 40px; + height: 40px; + margin-top: -2px; + margin-left: -5px; } .input-row-button:hover { background: var(--primary-hover); @@ -937,6 +962,17 @@ } .input-row-button-img { width: 24px; + height: 24px; + } + #send-button { + padding-top: 0; + padding-right: 3px; + } + #send-button-img { + width: 28px; + height: 28px; + background: var(--primary-hover); + border-radius: 50%; } .option-enabled { @@ -1106,7 +1142,7 @@ color: #f8fafc; border-radius: 2px; box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.4); - font-size small; + font-size: small; padding: 2px 4px; } } @@ -1126,6 +1162,9 @@ img.text-to-image { max-width: 100%; } + #clear-chat-button { + margin-left: 0; + } } @media only screen and (min-width: 600px) { body { From d4552117f6583f9817749262e3ae5337a2b93675 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 19 Jan 2024 23:40:50 +0530 Subject: [PATCH 03/11] Add and improve chat input pane, actions, icons on Obsidian client - Move delete icon to left of chat input. This makes it harder to inadvertently click - Add send button to chat footer. Enter being the only way to send messages is not intuitive, outside standard modern UI patterns - Color chat message send button to make it primary CTA on web client - Make chat footer shorter. Use no or round border on action buttons --- src/interface/obsidian/src/chat_modal.ts | 44 +++++++++++++++--------- src/interface/obsidian/styles.css | 28 ++++++--------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 0f597e2a..2ee2fa1a 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -17,17 +17,19 @@ export class KhojChatModal extends Modal { this.setting = setting; // Register Modal Keybindings to send user message - this.scope.register([], 'Enter', async () => { - // Get text in chat input elmenet - let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + this.scope.register([], 'Enter', async () => { await this.chat() }); + } - // Clear text after extracting message to send - let user_message = input_el.value; - input_el.value = ""; + async chat() { + // Get text in chat input element + let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; - // Get and render chat response to user message - await this.getChatResponse(user_message); - }); + // Clear text after extracting message to send + let user_message = input_el.value.trim(); + input_el.value = ""; + + // Get and render chat response to user message + await this.getChatResponse(user_message); } async onOpen() { @@ -42,10 +44,19 @@ export class KhojChatModal extends Modal { // Get chat history from Khoj backend let getChatHistorySucessfully = await this.getChatHistory(chatBodyEl); - let placeholderText = getChatHistorySucessfully ? "Chat with Khoj [Hit Enter to send message]" : "Configure Khoj to enable chat"; + let placeholderText = getChatHistorySucessfully ? "Message" : "Configure Khoj to enable chat"; // Add chat input field let inputRow = contentEl.createDiv("khoj-input-row"); + let clearChat = inputRow.createEl("button", { + text: "Clear History", + attr: { + class: "khoj-input-row-button clickable-icon", + }, + }) + clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); + setIcon(clearChat, "trash"); + let chatInput = inputRow.createEl("input", { attr: { type: "text", @@ -61,20 +72,21 @@ export class KhojChatModal extends Modal { text: "Transcribe", attr: { id: "khoj-transcribe", - class: "khoj-transcribe khoj-input-row-button", + class: "khoj-input-row-button clickable-icon ", }, }) transcribe.addEventListener('click', async (_) => { await this.speechToText() }); setIcon(transcribe, "mic"); - let clearChat = inputRow.createEl("button", { - text: "Clear History", + let send = inputRow.createEl("button", { + text: "Send", attr: { - class: "khoj-input-row-button", + id: "khoj-chat-send", + class: "khoj-input-row-button clickable-icon", }, }) - clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); - setIcon(clearChat, "trash"); + send.addEventListener('click', async (_) => { await this.chat() }); + setIcon(send, "arrow-up-circle"); // Scroll to bottom of modal, till the send message input box this.modalEl.scrollTop = this.modalEl.scrollHeight; diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 11fc0086..87325d61 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -230,36 +230,30 @@ img { } .khoj-input-row { display: grid; - grid-template-columns: auto 32px 32px; + grid-template-columns: 32px auto 32px 32px; grid-column-gap: 10px; grid-row-gap: 10px; background: var(--background-primary); + margin: 0 0 0 -8px; } #khoj-chat-input.option:hover { box-shadow: 0 0 11px var(--background-modifier-box-shadow); } #khoj-chat-input { font-size: var(--font-ui-medium); - padding: 25px 20px; + padding: 0 0 0 12px; + border-radius: 16px; + height: 32px; } .khoj-input-row-button { - background: var(--background-primary); - border: none; - border-radius: 5px; - padding: 5px; + border-radius: 50%; + padding: 4px; --icon-size: var(--icon-size); - height: auto; - font-size: 14px; - font-weight: 300; - line-height: 1.5em; - cursor: pointer; - transition: background 0.3s ease-in-out; } -.khoj-input-row-button:hover { - background: var(--background-modifier-hover); -} -.khoj-input-row-button:active { - background: var(--background-modifier-active); +#khoj-chat-send .svg-icon { + background: var(--khoj-sun); + border-radius: 50%; + color: #222; } @media (pointer: coarse), (hover: none) { From 07ca137bdf1cc6fd7c3f4244ec97cef7d9b23b24 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 00:24:28 +0530 Subject: [PATCH 04/11] Convert chat input into a text area in the Obsidian client This allows for better readability of multi-line messages by users. The chat input is a text area in the other clients as well. --- src/interface/obsidian/src/chat_modal.ts | 38 ++++++++++++++++++++---- src/interface/obsidian/styles.css | 5 +++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 2ee2fa1a..97b08620 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -22,11 +22,12 @@ export class KhojChatModal extends Modal { async chat() { // Get text in chat input element - let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; // Clear text after extracting message to send let user_message = input_el.value.trim(); input_el.value = ""; + this.autoResize(); // Get and render chat response to user message await this.getChatResponse(user_message); @@ -57,9 +58,8 @@ export class KhojChatModal extends Modal { clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); setIcon(clearChat, "trash"); - let chatInput = inputRow.createEl("input", { + let chatInput = inputRow.createEl("textarea", { attr: { - type: "text", id: "khoj-chat-input", autofocus: "autofocus", placeholder: placeholderText, @@ -67,12 +67,14 @@ export class KhojChatModal extends Modal { disabled: !getChatHistorySucessfully ? "disabled" : null }, }) + chatInput.addEventListener('input', (_) => { this.onChatInput() }); + chatInput.addEventListener('keydown', (event) => { this.incrementalChat(event) }); let transcribe = inputRow.createEl("button", { text: "Transcribe", attr: { id: "khoj-transcribe", - class: "khoj-input-row-button clickable-icon ", + class: "khoj-transcribe khoj-input-row-button clickable-icon ", }, }) transcribe.addEventListener('click', async (_) => { await this.speechToText() }); @@ -382,7 +384,7 @@ export class KhojChatModal extends Modal { flashStatusInChatInput(message: string) { // Get chat input element and original placeholder - let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; let originalPlaceholder = chatInput.placeholder; // Set placeholder to message chatInput.placeholder = message; @@ -420,7 +422,7 @@ export class KhojChatModal extends Modal { mediaRecorder: MediaRecorder | undefined; async speechToText() { const transcribeButton = this.contentEl.getElementsByClassName("khoj-transcribe")[0]; - const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; const generateRequestBody = async (audioBlob: Blob, boundary_string: string) => { const boundary = `------${boundary_string}`; @@ -494,4 +496,28 @@ export class KhojChatModal extends Modal { setIcon(transcribeButton, "mic"); } } + + incrementalChat(event: KeyboardEvent) { + if (!event.shiftKey && event.key === 'Enter') { + event.preventDefault(); + this.chat(); + } + } + + onChatInput() { + const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + chatInput.value = chatInput.value.trimStart(); + + this.autoResize(); + } + + autoResize() { + const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + const scrollTop = chatInput.scrollTop; + chatInput.style.height = '0'; + const scrollHeight = chatInput.scrollHeight + 8; // +8 accounts for padding + chatInput.style.height = Math.min(scrollHeight, 200) + 'px'; + chatInput.scrollTop = scrollTop; + this.modalEl.scrollTop = this.modalEl.scrollHeight; + } } diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 87325d61..4029ecc7 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -241,14 +241,17 @@ img { } #khoj-chat-input { font-size: var(--font-ui-medium); - padding: 0 0 0 12px; + padding: 4px 0 0 12px; border-radius: 16px; height: 32px; + resize: none; } .khoj-input-row-button { border-radius: 50%; padding: 4px; --icon-size: var(--icon-size); + height: 32px; + width: 32px; } #khoj-chat-send .svg-icon { background: var(--khoj-sun); From 8a488b9e396c6c8007ef79eac8417e4c04c98568 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 19 Jan 2024 20:05:29 +0530 Subject: [PATCH 05/11] Fix auto resizing chat input box when transcribed text added --- src/interface/desktop/chat.html | 5 +++-- src/interface/obsidian/src/chat_modal.ts | 3 ++- src/interface/obsidian/styles.css | 1 + src/khoj/interface/web/chat.html | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index ec5de0c7..10034385 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -690,7 +690,7 @@ fetch(url, { method: 'POST', body: formData, headers}) .then(response => response.ok ? response.json() : Promise.reject(response)) - .then(data => { chatInput.value += data.text; }) + .then(data => { chatInput.value += data.text.trimStart(); autoResize(); }) .catch(err => { if (err.status === 501) { flashStatusInChatInput("⛔️ Configure speech-to-text model on server.") @@ -917,7 +917,8 @@ grid-template-columns: 32px auto 32px 40px; grid-column-gap: 10px; grid-row-gap: 10px; - background: #f9fafc + background: #f9fafc; + align-items: center; } .option:hover { box-shadow: 0 0 11px #aaa; diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 97b08620..ae4159b2 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -453,7 +453,8 @@ export class KhojChatModal extends Modal { // Parse response from Khoj backend if (response.status === 200) { console.log(response); - chatInput.value += response.json.text; + chatInput.value += response.json.text.trimStart(); + this.autoResize(); } else if (response.status === 501) { throw new Error("⛔️ Configure speech-to-text model on server."); } else if (response.status === 422) { diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 4029ecc7..07739a23 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -235,6 +235,7 @@ img { grid-row-gap: 10px; background: var(--background-primary); margin: 0 0 0 -8px; + align-items: center; } #khoj-chat-input.option:hover { box-shadow: 0 0 11px var(--background-modifier-box-shadow); diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 54dc9692..14fb6057 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -651,7 +651,7 @@ To get started, just start typing below. You can also type / to see a list of co fetch('/api/transcribe?client=web', { method: 'POST', body: formData }) .then(response => response.ok ? response.json() : Promise.reject(response)) - .then(data => { chatInput.value += data.text; }) + .then(data => { chatInput.value += data.text.trimStart(); autoResize(); }) .catch(err => { if (err.status === 501) { flashStatusInChatInput("⛔️ Configure speech-to-text model on server.") @@ -987,6 +987,7 @@ To get started, just start typing below. You can also type / to see a list of co grid-column-gap: 10px; grid-row-gap: 10px; background: var(--background-color); + align-items: center; } .option:hover { box-shadow: 0 0 11px #aaa; From 7c8c475c3a92be44487550dc136fbfc1863cb39c Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 00:49:22 +0530 Subject: [PATCH 06/11] Add round border, hover color to starter questions in web, desktop apps --- src/interface/desktop/chat.html | 5 ++++- src/khoj/interface/web/chat.html | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 10034385..9fe4d14b 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -1015,7 +1015,7 @@ background: var(--background-color); color: var(--main-text-color); border: 1px solid var(--main-text-color); - border-radius: 5px; + border-radius: 16px; padding: 5px; font-size: 14px; font-weight: 300; @@ -1027,6 +1027,9 @@ transition: max-height 0.3s ease-in-out; overflow: hidden; } + button.question-starter:hover { + background: var(--primary-hover); + } code.chat-response { background: var(--primary-hover); diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 14fb6057..f371e00b 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -823,6 +823,9 @@ To get started, just start typing below. You can also type / to see a list of co transition: max-height 0.3s ease-in-out; overflow: hidden; } + button.question-starter:hover { + background: var(--primary-hover); + } button.reference-button { background: var(--background-color); From 26bd3533d83e4f5773b1b31ffb746fa4a430926a Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 03:27:18 +0530 Subject: [PATCH 07/11] Stop rendering empty starter questions element when no questions present --- src/interface/desktop/chat.html | 2 +- src/khoj/interface/web/chat.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 9fe4d14b..6a25497d 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -599,7 +599,7 @@ .then(response => response.json()) .then(data => { // Render chat options, if any - if (data) { + if (data.length > 0) { let questionStarterSuggestions = document.getElementById("question-starters"); for (let index in data) { let questionStarter = data[index]; diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index f371e00b..b8571d8e 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -581,7 +581,7 @@ To get started, just start typing below. You can also type / to see a list of co .then(response => response.json()) .then(data => { // Render chat options, if any - if (data) { + if (data.length > 0) { let questionStarterSuggestions = document.getElementById("question-starters"); for (let index in data) { let questionStarter = data[index]; From 699e9ff878423e8858463ed0ba03c914be365b49 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 03:12:16 +0530 Subject: [PATCH 08/11] Move to single click audio chat UX on web app - Capabillity New default UX has 1 long-press to send transcribed audio message - Removes the previous default of 3 clicks required to send audio message - The record > stop > send process to send audio messages was unclear - Still allows stopping message from being sent, if users want to make correction to transcribed audio - Removes inadvertent long audio transcriptions if user forgets to press stop when recording - Changes - Record audio while microphone button pressed - Show auto-send 3s countdown timer UI for audio chat message Provide a visual cue around send button for how long before audio message is automatically sent to Khoj for response - Auto-send msg in 3s unless stop send message button clicked --- src/khoj/interface/web/chat.html | 80 +++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index b8571d8e..306bb02f 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -639,10 +639,14 @@ To get started, just start typing below. You can also type / to see a list of co }); } + let sendMessageTimeout; let mediaRecorder; - function speechToText() { + function speechToText(event) { + event.preventDefault(); const speakButtonImg = document.getElementById('speak-button-img'); const stopRecordButtonImg = document.getElementById('stop-record-button-img'); + const sendButtonImg = document.getElementById('send-button-img'); + const stopSendButtonImg = document.getElementById('stop-send-button-img'); const chatInput = document.getElementById('chat-input'); const sendToServer = (audioBlob) => { @@ -652,6 +656,29 @@ To get started, just start typing below. You can also type / to see a list of co fetch('/api/transcribe?client=web', { method: 'POST', body: formData }) .then(response => response.ok ? response.json() : Promise.reject(response)) .then(data => { chatInput.value += data.text.trimStart(); autoResize(); }) + .then(() => { + // Don't auto-send empty messages + if (chatInput.value.length === 0) return; + + // Send message after 3 seconds, unless stop send button is clicked + sendButtonImg.style.display = 'none'; + stopSendButtonImg.style.display = 'initial'; + + // Start the countdown timer UI + document.getElementById('countdown-circle').style.animation = "countdown 3s linear 1 forwards"; + + sendMessageTimeout = setTimeout(() => { + // Revert to showing send-button and hide the stop-send-button + sendButtonImg.style.display = 'initial'; + stopSendButtonImg.style.display = 'none'; + + // Stop the countdown timer UI + document.getElementById('countdown-circle').style.animation = "none"; + + // Send message + chat(); + }, 3000); + }) .catch(err => { if (err.status === 501) { flashStatusInChatInput("⛔️ Configure speech-to-text model on server.") @@ -700,6 +727,18 @@ To get started, just start typing below. You can also type / to see a list of co stopRecordButtonImg.style.display = 'none'; } } + + function cancelSendMessage() { + // Cancel the chat() call if the stop-send-button is clicked + clearTimeout(sendMessageTimeout); + + // Revert to showing send-button and hide the stop-send-button + document.getElementById('stop-send-button-img').style.display = 'none'; + document.getElementById('send-button-img').style.display = 'initial'; + + // Stop the countdown timer UI + document.getElementById('countdown-circle').style.animation = "none"; + };
@@ -730,7 +769,8 @@ To get started, just start typing below. You can also type / to see a list of co - -
@@ -1037,8 +1081,8 @@ To get started, just start typing below. You can also type / to see a list of co height: 24px; } #send-button { - padding-top: 0; - padding-right: 3px; + padding: 0; + position: relative; } #send-button-img { width: 28px; @@ -1047,6 +1091,30 @@ To get started, just start typing below. You can also type / to see a list of co border-radius: 50%; } + #stop-send-button-img { + position: absolute; + top: 6px; + right: 6px; + width: 28px; + height: 28px; + transform: rotateY(-180deg) rotateZ(-90deg); + } + #countdown-circle { + stroke-dasharray: 44px; /* The circumference of the circle with 7px radius */ + stroke-dashoffset: 0px; + stroke-linecap: round; + stroke-width: 1px; + stroke: var(--main-text-color); + fill: none; + } + @keyframes countdown { + from { + stroke-dashoffset: 0px; + } + to { + stroke-dashoffset: -44px; /* The circumference of the circle with 7px radius */ + } + } .option-enabled { box-shadow: 0 0 12px rgb(119, 156, 46); From 29a581d2b0b1a312b51cc27c0601603cfc32a0aa Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 12:54:31 +0530 Subject: [PATCH 09/11] Move to single click audio chat UX on desktop app - Capabillity New default UX has 1 long-press to send transcribed audio message - Removes the previous default of 3 clicks required to send audio message - The record > stop > send process to send audio messages was unclear - Still allows stopping message from being sent, if users want to make correction to transcribed audio - Removes inadvertent long audio transcriptions if user forgets to press stop when recording - Changes - Record audio while microphone button pressed - Show auto-send 3s countdown timer UI for audio chat message Provide a visual cue around send button for how long before audio message is automatically sent to Khoj for response - Auto-send msg in 3s unless stop send message button clicked --- src/interface/desktop/chat.html | 79 ++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 6a25497d..01fbbfdc 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -673,10 +673,14 @@ }) } + let sendMessageTimeout; let mediaRecorder; - async function speechToText() { + async function speechToText(event) { + event.preventDefault(); const speakButtonImg = document.getElementById('speak-button-img'); const stopRecordButtonImg = document.getElementById('stop-record-button-img'); + const sendButtonImg = document.getElementById('send-button-img'); + const stopSendButtonImg = document.getElementById('stop-send-button-img'); const chatInput = document.getElementById('chat-input'); const hostURL = await window.hostURLAPI.getURL(); @@ -691,6 +695,29 @@ fetch(url, { method: 'POST', body: formData, headers}) .then(response => response.ok ? response.json() : Promise.reject(response)) .then(data => { chatInput.value += data.text.trimStart(); autoResize(); }) + .then(() => { + // Don't auto-send empty messages + if (chatInput.value.length === 0) return; + + // Send message after 3 seconds, unless stop send button is clicked + sendButtonImg.style.display = 'none'; + stopSendButtonImg.style.display = 'initial'; + + // Start the countdown timer UI + document.getElementById('countdown-circle').style.animation = "countdown 3s linear 1 forwards"; + + sendMessageTimeout = setTimeout(() => { + // Revert to showing send-button and hide the stop-send-button + sendButtonImg.style.display = 'initial'; + stopSendButtonImg.style.display = 'none'; + + // Stop the countdown timer UI + document.getElementById('countdown-circle').style.animation = "none"; + + // Send message + chat(); + }, 3000); + }) .catch(err => { if (err.status === 501) { flashStatusInChatInput("⛔️ Configure speech-to-text model on server.") @@ -738,6 +765,17 @@ } } + function cancelSendMessage() { + // Cancel the chat() call if the stop-send-button is clicked + clearTimeout(sendMessageTimeout); + + // Revert to showing send-button and hide the stop-send-button + document.getElementById('stop-send-button-img').style.display = 'none'; + document.getElementById('send-button-img').style.display = 'initial'; + + // Stop the countdown timer UI + document.getElementById('countdown-circle').style.animation = "none"; + };
@@ -776,7 +814,8 @@ - -
@@ -966,8 +1009,8 @@ height: 24px; } #send-button { - padding-top: 0; - padding-right: 3px; + padding: 0; + position: relative; } #send-button-img { width: 28px; @@ -975,6 +1018,30 @@ background: var(--primary-hover); border-radius: 50%; } + #stop-send-button-img { + position: absolute; + top: 6px; + right: 6px; + width: 28px; + height: 28px; + transform: rotateY(-180deg) rotateZ(-90deg); + } + #countdown-circle { + stroke-dasharray: 44px; /* The circumference of the circle with 7px radius */ + stroke-dashoffset: 0px; + stroke-linecap: round; + stroke-width: 1px; + stroke: var(--main-text-color); + fill: none; + } + @keyframes countdown { + from { + stroke-dashoffset: 0px; + } + to { + stroke-dashoffset: -44px; /* The circumference of the circle with 7px radius */ + } + } .option-enabled { box-shadow: 0 0 12px rgb(119, 156, 46); From f0daa45ae0fcc04f6500df2b2169985667fc565e Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 15:12:06 +0530 Subject: [PATCH 10/11] Move to single click audio chat UX on Obsidian client - Capabillity New default UX has 1 long-press to send transcribed audio message - Removes the previous default of 3 clicks required to send audio message - The record > stop > send process to send audio messages was unclear - Still allows stopping message from being sent, if users want to make correction to transcribed audio - Removes inadvertent long audio transcriptions if user forgets to press stop when recording - Changes - Record audio while microphone button pressed - Show auto-send 3s countdown timer UI for audio chat message Provide a visual cue around send button for how long before audio message is automatically sent to Khoj for response - Auto-send msg in 3s unless stop send message button clicked --- src/interface/obsidian/src/chat_modal.ts | 48 ++++++++++++++++++++++-- src/interface/obsidian/styles.css | 26 ++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index ae4159b2..dde29476 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -77,18 +77,22 @@ export class KhojChatModal extends Modal { class: "khoj-transcribe khoj-input-row-button clickable-icon ", }, }) - transcribe.addEventListener('click', async (_) => { await this.speechToText() }); + transcribe.addEventListener('mousedown', async (event) => { await this.speechToText(event) }); + transcribe.addEventListener('mouseup', async (event) => { await this.speechToText(event) }); + transcribe.addEventListener('touchstart', async (event) => { await this.speechToText(event) }); + transcribe.addEventListener('touchend', async (event) => { await this.speechToText(event) }); setIcon(transcribe, "mic"); let send = inputRow.createEl("button", { text: "Send", attr: { id: "khoj-chat-send", - class: "khoj-input-row-button clickable-icon", + class: "khoj-chat-send khoj-input-row-button clickable-icon", }, }) - send.addEventListener('click', async (_) => { await this.chat() }); setIcon(send, "arrow-up-circle"); + let sendImg = send.getElementsByClassName("lucide-arrow-up-circle")[0] + sendImg.addEventListener('click', async (_) => { await this.chat() }); // Scroll to bottom of modal, till the send message input box this.modalEl.scrollTop = this.modalEl.scrollHeight; @@ -419,10 +423,13 @@ export class KhojChatModal extends Modal { } } + sendMessageTimeout: NodeJS.Timeout | undefined; mediaRecorder: MediaRecorder | undefined; - async speechToText() { + async speechToText(event: MouseEvent | TouchEvent) { + event.preventDefault(); const transcribeButton = this.contentEl.getElementsByClassName("khoj-transcribe")[0]; const chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + const sendButton = this.modalEl.getElementsByClassName("khoj-chat-send")[0] const generateRequestBody = async (audioBlob: Blob, boundary_string: string) => { const boundary = `------${boundary_string}`; @@ -462,6 +469,28 @@ export class KhojChatModal extends Modal { } else { throw new Error("⛔️ Failed to transcribe audio."); } + + // Don't auto-send empty messages + if (chatInput.value.length === 0) return; + + // Show stop auto-send button. It stops auto-send when clicked + setIcon(sendButton, "stop-circle"); + let stopSendButtonImg = sendButton.getElementsByClassName("lucide-stop-circle")[0] + stopSendButtonImg.addEventListener('click', (_) => { this.cancelSendMessage() }); + + // Start the countdown timer UI + stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards"; + + // Auto send message after 3 seconds + this.sendMessageTimeout = setTimeout(() => { + // Stop the countdown timer UI + setIcon(sendButton, "arrow-up-circle") + let sendImg = sendButton.getElementsByClassName("lucide-arrow-up-circle")[0] + sendImg.addEventListener('click', async (_) => { await this.chat() }); + + // Send message + this.chat(); + }, 3000); }; const handleRecording = (stream: MediaStream) => { @@ -498,6 +527,17 @@ export class KhojChatModal extends Modal { } } + cancelSendMessage() { + // Cancel the auto-send chat message timer if the stop-send-button is clicked + clearTimeout(this.sendMessageTimeout); + + // Revert to showing send-button and hide the stop-send-button + let sendButton = this.modalEl.getElementsByClassName("khoj-chat-send")[0]; + setIcon(sendButton, "arrow-up-circle"); + let sendImg = sendButton.getElementsByClassName("lucide-arrow-up-circle")[0] + sendImg.addEventListener('click', async (_) => { await this.chat() }); + }; + incrementalChat(event: KeyboardEvent) { if (!event.shiftKey && event.key === 'Enter') { event.preventDefault(); diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 07739a23..3250ecc8 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -254,11 +254,35 @@ img { height: 32px; width: 32px; } -#khoj-chat-send .svg-icon { + +#khoj-chat-send { + padding: 0; + position: relative; +} +#khoj-chat-send .lucide-arrow-up-circle { background: var(--khoj-sun); border-radius: 50%; color: #222; } +#khoj-chat-send .lucide-stop-circle { + transform: rotateY(-180deg) rotateZ(-90deg); +} +#khoj-chat-send .lucide-stop-circle circle { + stroke-dasharray: 62px; /* The circumference of the circle with 7px radius */ + stroke-dashoffset: 0px; + stroke-linecap: round; + stroke-width: 2px; + stroke: var(--main-text-color); + fill: none; +} +@keyframes countdown { + from { + stroke-dashoffset: 0px; + } + to { + stroke-dashoffset: -62px; /* The circumference of the circle with 7px radius */ + } +} @media (pointer: coarse), (hover: none) { #khoj-chat-body.abbr[title] { From ec3b837d00c6ec9ec7e5f4b4bf07f358f5c546fb Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 21:28:20 +0530 Subject: [PATCH 11/11] Send audio message in 2-clicks on desktop to avoid holding down mic button --- src/interface/desktop/chat.html | 8 ++++---- src/interface/obsidian/src/chat_modal.ts | 8 ++++---- src/khoj/interface/web/chat.html | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 01fbbfdc..35605b64 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -749,14 +749,14 @@ }; // Toggle recording - if (!mediaRecorder || mediaRecorder.state === 'inactive') { + if (!mediaRecorder || mediaRecorder.state === 'inactive' || event.type === 'touchstart') { navigator.mediaDevices - .getUserMedia({ audio: true }) + ?.getUserMedia({ audio: true }) .then(handleRecording) .catch((e) => { flashStatusInChatInput("⛔️ Failed to access microphone"); }); - } else if (mediaRecorder.state === 'recording') { + } else if (mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') { mediaRecorder.stop(); mediaRecorder.stream.getTracks().forEach(track => track.stop()); mediaRecorder = null; @@ -815,7 +815,7 @@