mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 05:29:12 +00:00
Resolve merge conflicts for rendering chat response
This commit is contained in:
1
src/interface/desktop/assets/icons/microphone-solid.svg
Normal file
1
src/interface/desktop/assets/icons/microphone-solid.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M192 0C139 0 96 43 96 96V256c0 53 43 96 96 96s96-43 96-96V96c0-53-43-96-96-96zM64 216c0-13.3-10.7-24-24-24s-24 10.7-24 24v40c0 89.1 66.2 162.7 152 174.4V464H120c-13.3 0-24 10.7-24 24s10.7 24 24 24h72 72c13.3 0 24-10.7 24-24s-10.7-24-24-24H216V430.4c85.8-11.7 152-85.3 152-174.4V216c0-13.3-10.7-24-24-24s-24 10.7-24 24v40c0 70.7-57.3 128-128 128s-128-57.3-128-128V216z"/></svg>
|
||||
|
After Width: | Height: | Size: 616 B |
37
src/interface/desktop/assets/icons/stop-solid.svg
Normal file
37
src/interface/desktop/assets/icons/stop-solid.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 384 512"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="stop-solid.svg"
|
||||
inkscape:version="1.3 (0e150ed, 2023-07-21)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="192"
|
||||
inkscape:cy="256"
|
||||
inkscape:window-width="1312"
|
||||
inkscape:window-height="449"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="88"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1" />
|
||||
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path
|
||||
d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"
|
||||
id="path1"
|
||||
style="fill:#aa0000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -292,14 +292,13 @@
|
||||
.then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let rawResponse = "";
|
||||
let references = null;
|
||||
|
||||
function readStream() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
// Evaluate the contents of new_response_text.innerHTML after all the data has been streamed
|
||||
const currentHTML = newResponseText.innerHTML;
|
||||
newResponseText.innerHTML = formatHTMLMessage(currentHTML);
|
||||
// Append any references after all the data has been streamed
|
||||
newResponseText.appendChild(references);
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
return;
|
||||
@@ -310,14 +309,15 @@
|
||||
|
||||
if (chunk.includes("### compiled references:")) {
|
||||
const additionalResponse = chunk.split("### compiled references:")[0];
|
||||
newResponseText.innerHTML += additionalResponse;
|
||||
rawResponse += additionalResponse;
|
||||
newResponseText.innerHTML = "";
|
||||
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
||||
|
||||
const rawReference = chunk.split("### compiled references:")[1];
|
||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||
references = document.createElement('div');
|
||||
references.classList.add("references");
|
||||
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
referenceExpandButton.classList.add("reference-expand-button");
|
||||
|
||||
@@ -374,7 +374,10 @@
|
||||
}
|
||||
} else {
|
||||
// If the chunk is not a JSON object, just display it as is
|
||||
newResponseText.innerHTML += chunk;
|
||||
rawResponse += chunk;
|
||||
newResponseText.innerHTML = "";
|
||||
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
||||
|
||||
readStream();
|
||||
}
|
||||
}
|
||||
@@ -529,6 +532,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
function flashStatusInChatInput(message) {
|
||||
// Get chat input element and original placeholder
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
// Set placeholder to message
|
||||
chatInput.placeholder = message;
|
||||
// Reset placeholder after 2 seconds
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async function clearConversationHistory() {
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
@@ -543,17 +558,71 @@
|
||||
.then(data => {
|
||||
chatBody.innerHTML = "";
|
||||
loadChat();
|
||||
chatInput.placeholder = "Cleared conversation history";
|
||||
flashStatusInChatInput("🗑 Cleared conversation history");
|
||||
})
|
||||
.catch(err => {
|
||||
chatInput.placeholder = "Failed to clear conversation history";
|
||||
flashStatusInChatInput("⛔️ Failed to clear conversation history");
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
let mediaRecorder;
|
||||
async function speechToText() {
|
||||
const speakButtonImg = document.getElementById('speak-button-img');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
|
||||
const hostURL = await window.hostURLAPI.getURL();
|
||||
let url = `${hostURL}/api/transcribe?client=desktop`;
|
||||
const khojToken = await window.tokenAPI.getToken();
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
const sendToServer = (audioBlob) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', audioBlob);
|
||||
|
||||
fetch(url, { method: 'POST', body: formData, headers})
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => { chatInput.value += data.text; })
|
||||
.catch(err => {
|
||||
err.status == 422
|
||||
? flashStatusInChatInput("⛔️ Configure speech-to-text model on server.")
|
||||
: flashStatusInChatInput("⛔️ Failed to transcribe audio")
|
||||
});
|
||||
};
|
||||
|
||||
const handleRecording = (stream) => {
|
||||
const audioChunks = [];
|
||||
const recordingConfig = { mimeType: 'audio/webm' };
|
||||
mediaRecorder = new MediaRecorder(stream, recordingConfig);
|
||||
|
||||
mediaRecorder.addEventListener("dataavailable", function(event) {
|
||||
if (event.data.size > 0) audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
mediaRecorder.addEventListener("stop", function() {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
sendToServer(audioBlob);
|
||||
});
|
||||
|
||||
mediaRecorder.start();
|
||||
speakButtonImg.src = './assets/icons/stop-solid.svg';
|
||||
speakButtonImg.alt = 'Stop Transcription';
|
||||
};
|
||||
|
||||
// Toggle recording
|
||||
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then(handleRecording)
|
||||
.catch((e) => {
|
||||
flashStatusInChatInput("⛔️ Failed to access microphone");
|
||||
});
|
||||
} else if (mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
speakButtonImg.src = './assets/icons/microphone-solid.svg';
|
||||
speakButtonImg.alt = 'Transcribe';
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<body>
|
||||
<div id="khoj-empty-container" class="khoj-empty-container">
|
||||
@@ -582,8 +651,11 @@
|
||||
<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 class="input-row-button" onclick="clearConversationHistory()">
|
||||
<img class="input-rown-button-img" src="./assets/icons/trash-solid.svg" alt="Clear Chat History"></img>
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -633,7 +705,6 @@
|
||||
.chat-message.you {
|
||||
margin-right: auto;
|
||||
text-align: right;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* basic style chat message text */
|
||||
.chat-message-text {
|
||||
@@ -650,7 +721,6 @@
|
||||
color: var(--primary-inverse);
|
||||
background: var(--primary);
|
||||
margin-left: auto;
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* Spinner symbol when the chat message is loading */
|
||||
.spinner {
|
||||
@@ -707,7 +777,7 @@
|
||||
}
|
||||
#input-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 32px;
|
||||
grid-template-columns: auto 32px 32px;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
background: #f9fafc
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, Modal, request, setIcon } from 'obsidian';
|
||||
import { App, Modal, RequestUrlParam, request, requestUrl, setIcon } from 'obsidian';
|
||||
import { KhojSetting } from 'src/settings';
|
||||
import fetch from "node-fetch";
|
||||
|
||||
@@ -51,6 +51,16 @@ export class KhojChatModal extends Modal {
|
||||
})
|
||||
chatInput.addEventListener('change', (event) => { this.result = (<HTMLInputElement>event.target).value });
|
||||
|
||||
let transcribe = inputRow.createEl("button", {
|
||||
text: "Transcribe",
|
||||
attr: {
|
||||
id: "khoj-transcribe",
|
||||
class: "khoj-transcribe khoj-input-row-button",
|
||||
},
|
||||
})
|
||||
transcribe.addEventListener('click', async (_) => { await this.speechToText() });
|
||||
setIcon(transcribe, "mic");
|
||||
|
||||
let clearChat = inputRow.createEl("button", {
|
||||
text: "Clear History",
|
||||
attr: {
|
||||
@@ -205,9 +215,19 @@ export class KhojChatModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
async clearConversationHistory() {
|
||||
flashStatusInChatInput(message: string) {
|
||||
// Get chat input element and original placeholder
|
||||
let chatInput = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
// Set placeholder to message
|
||||
chatInput.placeholder = message;
|
||||
// Reset placeholder after 2 seconds
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
async clearConversationHistory() {
|
||||
let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0];
|
||||
|
||||
let response = await request({
|
||||
@@ -224,15 +244,84 @@ export class KhojChatModal extends Modal {
|
||||
// If conversation history is cleared successfully, clear chat logs from modal
|
||||
chatBody.innerHTML = "";
|
||||
await this.getChatHistory();
|
||||
chatInput.placeholder = result.message;
|
||||
this.flashStatusInChatInput(result.message);
|
||||
}
|
||||
} catch (err) {
|
||||
chatInput.placeholder = "Failed to clear conversation history";
|
||||
} finally {
|
||||
// Reset to original placeholder text after some time
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
this.flashStatusInChatInput("Failed to clear conversation history");
|
||||
}
|
||||
}
|
||||
|
||||
mediaRecorder: MediaRecorder | undefined;
|
||||
async speechToText() {
|
||||
const transcribeButton = <HTMLButtonElement>this.contentEl.getElementsByClassName("khoj-transcribe")[0];
|
||||
const chatInput = <HTMLInputElement>this.contentEl.getElementsByClassName("khoj-chat-input")[0];
|
||||
|
||||
const generateRequestBody = async (audioBlob: Blob, boundary_string: string) => {
|
||||
const boundary = `------${boundary_string}`;
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
|
||||
chunks.push(new TextEncoder().encode(`${boundary}\r\n`));
|
||||
chunks.push(new TextEncoder().encode(`Content-Disposition: form-data; name="file"; filename="blob"\r\nContent-Type: "application/octet-stream"\r\n\r\n`));
|
||||
chunks.push(await audioBlob.arrayBuffer());
|
||||
chunks.push(new TextEncoder().encode('\r\n'));
|
||||
|
||||
await Promise.all(chunks);
|
||||
chunks.push(new TextEncoder().encode(`${boundary}--\r\n`));
|
||||
return await new Blob(chunks).arrayBuffer();
|
||||
};
|
||||
|
||||
const sendToServer = async (audioBlob: Blob) => {
|
||||
const boundary_string = `Boundary${Math.random().toString(36).slice(2)}`;
|
||||
const requestBody = await generateRequestBody(audioBlob, boundary_string);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `${this.setting.khojUrl}/api/transcribe?client=obsidian`,
|
||||
method: 'POST',
|
||||
headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` },
|
||||
contentType: `multipart/form-data; boundary=----${boundary_string}`,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
// Parse response from Khoj backend
|
||||
if (response.status === 200) {
|
||||
console.log(response);
|
||||
chatInput.value += response.json.text;
|
||||
} else if (response.status === 422) {
|
||||
throw new Error("⛔️ Failed to transcribe audio");
|
||||
} else {
|
||||
throw new Error("⛔️ Configure speech-to-text model on server.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecording = (stream: MediaStream) => {
|
||||
const audioChunks: Blob[] = [];
|
||||
const recordingConfig = { mimeType: 'audio/webm' };
|
||||
this.mediaRecorder = new MediaRecorder(stream, recordingConfig);
|
||||
|
||||
this.mediaRecorder.addEventListener("dataavailable", function(event) {
|
||||
if (event.data.size > 0) audioChunks.push(event.data);
|
||||
});
|
||||
|
||||
this.mediaRecorder.addEventListener("stop", async function() {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
await sendToServer(audioBlob);
|
||||
});
|
||||
|
||||
this.mediaRecorder.start();
|
||||
setIcon(transcribeButton, "mic-off");
|
||||
};
|
||||
|
||||
// Toggle recording
|
||||
if (!this.mediaRecorder || this.mediaRecorder.state === 'inactive') {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ audio: true })
|
||||
.then(handleRecording)
|
||||
.catch((e) => {
|
||||
this.flashStatusInChatInput("⛔️ Failed to access microphone");
|
||||
});
|
||||
} else if (this.mediaRecorder.state === 'recording') {
|
||||
this.mediaRecorder.stop();
|
||||
setIcon(transcribeButton, "mic");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ If your plugin does not need CSS, delete this file.
|
||||
}
|
||||
.khoj-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 32px;
|
||||
grid-template-columns: auto 32px 32px;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
background: var(--background-primary);
|
||||
|
||||
Reference in New Issue
Block a user