From f0daa45ae0fcc04f6500df2b2169985667fc565e Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 20 Jan 2024 15:12:06 +0530 Subject: [PATCH] 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] {