diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 7cd75f01..35605b64 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; @@ -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]; @@ -673,9 +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(); @@ -689,7 +694,30 @@ 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(); }) + .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.") @@ -716,27 +744,38 @@ }); mediaRecorder.start(); - speakButtonImg.src = './assets/icons/stop-solid.svg'; - speakButtonImg.alt = 'Stop Transcription'; + speakButtonImg.style.display = 'none'; + stopRecordButtonImg.style.display = 'initial'; }; // 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; - speakButtonImg.src = './assets/icons/microphone-solid.svg'; - speakButtonImg.alt = 'Transcribe'; + speakButtonImg.style.display = 'initial'; + 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"; + };
@@ -764,12 +803,36 @@ @@ -894,10 +957,11 @@ } #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 + background: #f9fafc; + align-items: center; } .option:hover { box-shadow: 0 0 11px #aaa; @@ -905,12 +969,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 +984,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 +1006,41 @@ } .input-row-button-img { width: 24px; + height: 24px; + } + #send-button { + padding: 0; + position: relative; + } + #send-button-img { + width: 28px; + height: 28px; + 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 { @@ -978,7 +1082,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; @@ -990,6 +1094,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); @@ -1106,7 +1213,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 +1233,9 @@ img.text-to-image { max-width: 100%; } + #clear-chat-button { + margin-left: 0; + } } @media only screen and (min-width: 600px) { body { diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 0f597e2a..1b35f499 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -17,17 +17,20 @@ 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 = ""; + this.autoResize(); + + // Get and render chat response to user message + await this.getChatResponse(user_message); } async onOpen() { @@ -42,13 +45,21 @@ 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 chatInput = inputRow.createEl("input", { + 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("textarea", { attr: { - type: "text", id: "khoj-chat-input", autofocus: "autofocus", placeholder: placeholderText, @@ -56,25 +67,32 @@ 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-transcribe khoj-input-row-button", + 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('touchstart', async (event) => { await this.speechToText(event) }); + transcribe.addEventListener('touchend', async (event) => { await this.speechToText(event) }); + transcribe.addEventListener('touchcancel', async (event) => { await this.speechToText(event) }); 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-chat-send khoj-input-row-button clickable-icon", }, }) - clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); - setIcon(clearChat, "trash"); + 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; @@ -370,7 +388,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; @@ -405,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 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}`; @@ -439,7 +460,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) { @@ -447,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) => { @@ -468,18 +512,53 @@ export class KhojChatModal extends Modal { }; // Toggle recording - if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') { + if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive' || event.type === 'touchstart') { navigator.mediaDevices .getUserMedia({ audio: true }) - .then(handleRecording) + ?.then(handleRecording) .catch((e) => { this.flashStatusInChatInput("⛔️ Failed to access microphone"); }); - } else if (this.mediaRecorder.state === 'recording') { + } else if (this.mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') { this.mediaRecorder.stop(); this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); this.mediaRecorder = undefined; setIcon(transcribeButton, "mic"); } } + + 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(); + 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 11fc0086..3250ecc8 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -230,36 +230,58 @@ 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; + align-items: center; } #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: 4px 0 0 12px; + border-radius: 16px; + height: 32px; + resize: none; } .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; + height: 32px; + width: 32px; } -.khoj-input-row-button:hover { - background: var(--background-modifier-hover); + +#khoj-chat-send { + padding: 0; + position: relative; } -.khoj-input-row-button:active { - background: var(--background-modifier-active); +#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) { diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 2a813779..19853be5 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; @@ -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]; @@ -639,9 +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) => { @@ -650,7 +655,30 @@ 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(); }) + .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.") @@ -679,26 +707,38 @@ 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 - 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; - speakButtonImg.src = '/static/assets/icons/microphone-solid.svg'; - speakButtonImg.alt = 'Transcribe'; + speakButtonImg.style.display = 'initial'; + 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"; + };
@@ -718,12 +758,36 @@ To get started, just start typing below. You can also type / to see a list of co @@ -803,6 +867,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); @@ -963,10 +1030,11 @@ 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); + align-items: center; } .option:hover { box-shadow: 0 0 11px #aaa; @@ -974,12 +1042,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 +1057,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,8 +1078,43 @@ 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: 0; + position: relative; + } + #send-button-img { + width: 28px; + height: 28px; + 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); @@ -1083,6 +1190,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 {