mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-23 23:48:56 +01:00
Improve Chat Input Pane Actions. Move to 1 Click Audio Chat on Mobile (#624)
## Major ### Move to single click audio chat UX on Obsidian, Desktop, Web clients New default UX has 1 long-press on mobile, 2-click on desktop to send transcribed audio message - New Audio Chat Flow 1. Record audio while microphone button pressed 2. 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 3. Auto-send msg in 3s unless stop send message button clicked - Why - Removes the previous default of 3 clicks required to send audio message The record > stop > send process to send audio messages was unclear and effortful - Still allows stopping message from being sent, to make correction to transcribed audio - Removes inadvertent long audio transcriptions if forget to press stop while recording ### Improve chat input pane actions & icons on Obsidian. Desktop, Web clients - Use SVG icons in chat footer on web, desktop app - Move delete icon to left of chat input. This makes it harder to inadvertently click it - Add send button to chat input pane - Color chat message send button to make it primary CTA - Make chat footer shorter. Use no or round border on action buttons ## Minor - Stop rendering empty starter questions element when no questions present - Add round border, hover color to starter questions in web, desktop apps - Fix auto resizing chat input box when transcribed text added - Convert chat input into a text area in the Obsidian client
This commit is contained in:
commit
679f0f24a4
4 changed files with 411 additions and 90 deletions
|
@ -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";
|
||||
};
|
||||
</script>
|
||||
<body>
|
||||
<div id="khoj-empty-container" class="khoj-empty-container">
|
||||
|
@ -764,12 +803,36 @@
|
|||
<div id="chat-footer">
|
||||
<div id="chat-tooltip" style="display: none;"></div>
|
||||
<div id="input-row">
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea>
|
||||
<button id="speak-button" class="input-row-button" onclick="speechToText()">
|
||||
<img id="speak-button-img" class="input-row-button-img" src="./assets/icons/microphone-solid.svg" alt="Transcribe"></img>
|
||||
<button id="clear-chat-button" class="input-row-button" onclick="clearConversationHistory()">
|
||||
<svg class="input-row-button-img" alt="Clear Chat History" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<rect width="128" height="128" fill="none"/>
|
||||
<line x1="216" y1="56" x2="40" y2="56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="104" y1="104" x2="104" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="152" y1="104" x2="152" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M168,56V40a16,16,0,0,0-16-16H104A16,16,0,0,0,88,40V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="clear-chat" class="input-row-button" onclick="clearConversationHistory()">
|
||||
<img class="input-row-button-img" src="./assets/icons/trash-solid.svg" alt="Clear Chat History"></img>
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Message"></textarea>
|
||||
<button id="speak-button" class="input-row-button"
|
||||
ontouchstart="speechToText(event)" ontouchend="speechToText(event)" ontouchcancel="speechToText(event)" onmousedown="speechToText(event)">
|
||||
<svg id="speak-button-img" class="input-row-button-img" alt="Transcribe" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
|
||||
</svg>
|
||||
<svg id="stop-record-button-img" style="display: none" class="input-row-button-img" alt="Stop Transcribing" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="send-button" class="input-row-button" alt="Send message">
|
||||
<svg id="send-button-img" onclick="chat()" class="input-row-button-img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-7.5 3.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V11.5z"/>
|
||||
</svg>
|
||||
<svg id="stop-send-button-img" onclick="cancelSendMessage()" style="display: none" class="input-row-button-img" alt="Stop Message Send" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<circle id="countdown-circle" class="countdown-circle" cx="8" cy="8" r="7" />
|
||||
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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 = <HTMLInputElement>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 = <HTMLTextAreaElement>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 = <SVGElement>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 = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
let chatInput = <HTMLTextAreaElement>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 = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-transcribe")[0];
|
||||
const chatInput = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
const chatInput = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
const sendButton = <HTMLButtonElement>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 = <SVGElement>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 = <SVGElement>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 = <HTMLButtonElement>this.modalEl.getElementsByClassName("khoj-chat-send")[0];
|
||||
setIcon(sendButton, "arrow-up-circle");
|
||||
let sendImg = <SVGElement>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 = <HTMLTextAreaElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
chatInput.value = chatInput.value.trimStart();
|
||||
|
||||
this.autoResize();
|
||||
}
|
||||
|
||||
autoResize() {
|
||||
const chatInput = <HTMLTextAreaElement>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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
</script>
|
||||
<body>
|
||||
<div id="khoj-empty-container" class="khoj-empty-container">
|
||||
|
@ -718,12 +758,36 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
<div id="chat-footer">
|
||||
<div id="chat-tooltip" style="display: none;"></div>
|
||||
<div id="input-row">
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea>
|
||||
<button id="speak-button" class="input-row-button" onclick="speechToText()">
|
||||
<img id="speak-button-img" class="input-row-button-img" src="/static/assets/icons/microphone-solid.svg" alt="Transcribe"></img>
|
||||
<button id="clear-chat-button" class="input-row-button" onclick="clearConversationHistory()">
|
||||
<svg class="input-row-button-img" alt="Clear Chat History" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<rect width="128" height="128" fill="none"/>
|
||||
<line x1="216" y1="56" x2="40" y2="56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="104" y1="104" x2="104" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="152" y1="104" x2="152" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M168,56V40a16,16,0,0,0-16-16H104A16,16,0,0,0,88,40V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="input-row-button" onclick="clearConversationHistory()">
|
||||
<img class="input-row-button-img" src="/static/assets/icons/trash-solid.svg" alt="Clear Chat History"></img>
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Message"></textarea>
|
||||
<button id="speak-button" class="input-row-button"
|
||||
ontouchstart="speechToText(event)" ontouchend="speechToText(event)" ontouchcancel="speechToText(event)" onmousedown="speechToText(event)">
|
||||
<svg id="speak-button-img" class="input-row-button-img" alt="Transcribe" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
|
||||
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
|
||||
</svg>
|
||||
<svg id="stop-record-button-img" style="display: none" class="input-row-button-img" alt="Stop Transcribing" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="send-button" class="input-row-button" alt="Send message">
|
||||
<svg id="send-button-img" onclick="chat()" class="input-row-button-img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-7.5 3.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V11.5z"/>
|
||||
</svg>
|
||||
<svg id="stop-send-button-img" onclick="cancelSendMessage()" style="display: none" class="input-row-button-img" alt="Stop Message Send" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<circle id="countdown-circle" class="countdown-circle" cx="8" cy="8" r="7" />
|
||||
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue