mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 16:14:21 +00:00
Share desktop chat streaming utility funcs across chat, shortcut views
Null check menu, menuContainer to avoid errors on Khoj mini
This commit is contained in:
parent
e439a6ddac
commit
0277d16daf
4 changed files with 247 additions and 337 deletions
|
@ -167,222 +167,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLoadingEllipsis() {
|
|
||||||
let loadingEllipsis = document.createElement("div");
|
|
||||||
loadingEllipsis.classList.add("lds-ellipsis");
|
|
||||||
|
|
||||||
let firstEllipsis = document.createElement("div");
|
|
||||||
firstEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let secondEllipsis = document.createElement("div");
|
|
||||||
secondEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let thirdEllipsis = document.createElement("div");
|
|
||||||
thirdEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let fourthEllipsis = document.createElement("div");
|
|
||||||
fourthEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
loadingEllipsis.appendChild(firstEllipsis);
|
|
||||||
loadingEllipsis.appendChild(secondEllipsis);
|
|
||||||
loadingEllipsis.appendChild(thirdEllipsis);
|
|
||||||
loadingEllipsis.appendChild(fourthEllipsis);
|
|
||||||
|
|
||||||
return loadingEllipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamResponse(newResponseElement, rawResponse, rawQuery, loadingEllipsis, replace=true) {
|
|
||||||
if (!newResponseElement) return;
|
|
||||||
// Remove loading ellipsis if it exists
|
|
||||||
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
|
|
||||||
newResponseElement.removeChild(loadingEllipsis);
|
|
||||||
// Clear the response element if replace is true
|
|
||||||
if (replace) newResponseElement.innerHTML = "";
|
|
||||||
|
|
||||||
// Append response to the response element
|
|
||||||
newResponseElement.appendChild(formatHTMLMessage(rawResponse, false, replace, rawQuery));
|
|
||||||
|
|
||||||
// Append loading ellipsis if it exists
|
|
||||||
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
|
|
||||||
// Scroll to bottom of chat view
|
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleImageResponse(imageJson, rawResponse) {
|
|
||||||
if (imageJson.image) {
|
|
||||||
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
|
||||||
|
|
||||||
// If response has image field, response is a generated image.
|
|
||||||
if (imageJson.intentType === "text-to-image") {
|
|
||||||
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
|
|
||||||
} else if (imageJson.intentType === "text-to-image2") {
|
|
||||||
rawResponse += `![generated_image](${imageJson.image})`;
|
|
||||||
} else if (imageJson.intentType === "text-to-image-v3") {
|
|
||||||
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
|
|
||||||
}
|
|
||||||
if (inferredQuery) {
|
|
||||||
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If response has detail field, response is an error message.
|
|
||||||
if (imageJson.detail) rawResponse += imageJson.detail;
|
|
||||||
|
|
||||||
return rawResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizeChatBodyResponse(references, newResponseElement) {
|
|
||||||
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
|
||||||
newResponseElement.appendChild(createReferenceSection(references));
|
|
||||||
}
|
|
||||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
||||||
document.getElementById("chat-input")?.removeAttribute("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
function collectJsonsInBufferedMessageChunk(chunk) {
|
|
||||||
// Collect list of JSON objects and raw strings in the chunk
|
|
||||||
// Return the list of objects and the remaining raw string
|
|
||||||
let startIndex = chunk.indexOf('{');
|
|
||||||
if (startIndex === -1) return { objects: [chunk], remainder: '' };
|
|
||||||
const objects = [chunk.slice(0, startIndex)];
|
|
||||||
let openBraces = 0;
|
|
||||||
let currentObject = '';
|
|
||||||
|
|
||||||
for (let i = startIndex; i < chunk.length; i++) {
|
|
||||||
if (chunk[i] === '{') {
|
|
||||||
if (openBraces === 0) startIndex = i;
|
|
||||||
openBraces++;
|
|
||||||
}
|
|
||||||
if (chunk[i] === '}') {
|
|
||||||
openBraces--;
|
|
||||||
if (openBraces === 0) {
|
|
||||||
currentObject = chunk.slice(startIndex, i + 1);
|
|
||||||
objects.push(currentObject);
|
|
||||||
currentObject = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
objects: objects,
|
|
||||||
remainder: openBraces > 0 ? chunk.slice(startIndex) : ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertMessageChunkToJson(rawChunk) {
|
|
||||||
// Split the chunk into lines
|
|
||||||
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
|
|
||||||
try {
|
|
||||||
let jsonChunk = JSON.parse(rawChunk);
|
|
||||||
if (!jsonChunk.type)
|
|
||||||
jsonChunk = {type: 'message', data: jsonChunk};
|
|
||||||
return jsonChunk;
|
|
||||||
} catch (e) {
|
|
||||||
return {type: 'message', data: rawChunk};
|
|
||||||
}
|
|
||||||
} else if (rawChunk.length > 0) {
|
|
||||||
return {type: 'message', data: rawChunk};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processMessageChunk(rawChunk) {
|
|
||||||
const chunk = convertMessageChunkToJson(rawChunk);
|
|
||||||
console.debug("Chunk:", chunk);
|
|
||||||
if (!chunk || !chunk.type) return;
|
|
||||||
if (chunk.type ==='status') {
|
|
||||||
console.log(`status: ${chunk.data}`);
|
|
||||||
const statusMessage = chunk.data;
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, statusMessage, chatMessageState.rawQuery, chatMessageState.loadingEllipsis, false);
|
|
||||||
} else if (chunk.type === 'start_llm_response') {
|
|
||||||
console.log("Started streaming", new Date());
|
|
||||||
} else if (chunk.type === 'end_llm_response') {
|
|
||||||
console.log("Stopped streaming", new Date());
|
|
||||||
|
|
||||||
// Automatically respond with voice if the subscribed user has sent voice message
|
|
||||||
if (chatMessageState.isVoice && "{{ is_active }}" == "True")
|
|
||||||
textToSpeech(chatMessageState.rawResponse);
|
|
||||||
|
|
||||||
// Append any references after all the data has been streamed
|
|
||||||
finalizeChatBodyResponse(chatMessageState.references, chatMessageState.newResponseTextEl);
|
|
||||||
|
|
||||||
const liveQuery = chatMessageState.rawQuery;
|
|
||||||
// Reset variables
|
|
||||||
chatMessageState = {
|
|
||||||
newResponseTextEl: null,
|
|
||||||
newResponseEl: null,
|
|
||||||
loadingEllipsis: null,
|
|
||||||
references: {},
|
|
||||||
rawResponse: "",
|
|
||||||
rawQuery: liveQuery,
|
|
||||||
isVoice: false,
|
|
||||||
}
|
|
||||||
} else if (chunk.type === "references") {
|
|
||||||
chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.online_results};
|
|
||||||
} else if (chunk.type === 'message') {
|
|
||||||
const chunkData = chunk.data;
|
|
||||||
if (typeof chunkData === 'object' && chunkData !== null) {
|
|
||||||
// If chunkData is already a JSON object
|
|
||||||
handleJsonResponse(chunkData);
|
|
||||||
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
|
|
||||||
// Try process chunk data as if it is a JSON object
|
|
||||||
try {
|
|
||||||
const jsonData = JSON.parse(chunkData.trim());
|
|
||||||
handleJsonResponse(jsonData);
|
|
||||||
} catch (e) {
|
|
||||||
chatMessageState.rawResponse += chunkData;
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chatMessageState.rawResponse += chunkData;
|
|
||||||
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleJsonResponse(jsonData) {
|
|
||||||
if (jsonData.image || jsonData.detail) {
|
|
||||||
chatMessageState.rawResponse = handleImageResponse(jsonData, chatMessageState.rawResponse);
|
|
||||||
} else if (jsonData.response) {
|
|
||||||
chatMessageState.rawResponse = jsonData.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chatMessageState.newResponseTextEl) {
|
|
||||||
chatMessageState.newResponseTextEl.innerHTML = "";
|
|
||||||
chatMessageState.newResponseTextEl.appendChild(formatHTMLMessage(chatMessageState.rawResponse));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readChatStream(response) {
|
|
||||||
if (!response.body) return;
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
let netBracketCount = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = await reader.read();
|
|
||||||
// If the stream is done
|
|
||||||
if (done) {
|
|
||||||
// Process the last chunk
|
|
||||||
processMessageChunk(buffer);
|
|
||||||
buffer = '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read chunk from stream and append it to the buffer
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
buffer += chunk;
|
|
||||||
|
|
||||||
// Check if the buffer contains (0 or more) complete JSON objects
|
|
||||||
netBracketCount += (chunk.match(/{/g) || []).length - (chunk.match(/}/g) || []).length;
|
|
||||||
if (netBracketCount === 0) {
|
|
||||||
let chunks = collectJsonsInBufferedMessageChunk(buffer);
|
|
||||||
chunks.objects.forEach((chunk) => processMessageChunk(chunk));
|
|
||||||
buffer = chunks.remainder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function incrementalChat(event) {
|
function incrementalChat(event) {
|
||||||
if (!event.shiftKey && event.key === 'Enter') {
|
if (!event.shiftKey && event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
|
@ -364,3 +364,219 @@ function createReferenceSection(references, createLinkerSection=false) {
|
||||||
|
|
||||||
return referencesDiv;
|
return referencesDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createLoadingEllipsis() {
|
||||||
|
let loadingEllipsis = document.createElement("div");
|
||||||
|
loadingEllipsis.classList.add("lds-ellipsis");
|
||||||
|
|
||||||
|
let firstEllipsis = document.createElement("div");
|
||||||
|
firstEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let secondEllipsis = document.createElement("div");
|
||||||
|
secondEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let thirdEllipsis = document.createElement("div");
|
||||||
|
thirdEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let fourthEllipsis = document.createElement("div");
|
||||||
|
fourthEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
loadingEllipsis.appendChild(firstEllipsis);
|
||||||
|
loadingEllipsis.appendChild(secondEllipsis);
|
||||||
|
loadingEllipsis.appendChild(thirdEllipsis);
|
||||||
|
loadingEllipsis.appendChild(fourthEllipsis);
|
||||||
|
|
||||||
|
return loadingEllipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStreamResponse(newResponseElement, rawResponse, rawQuery, loadingEllipsis, replace=true) {
|
||||||
|
if (!newResponseElement) return;
|
||||||
|
// Remove loading ellipsis if it exists
|
||||||
|
if (newResponseElement.getElementsByClassName("lds-ellipsis").length > 0 && loadingEllipsis)
|
||||||
|
newResponseElement.removeChild(loadingEllipsis);
|
||||||
|
// Clear the response element if replace is true
|
||||||
|
if (replace) newResponseElement.innerHTML = "";
|
||||||
|
|
||||||
|
// Append response to the response element
|
||||||
|
newResponseElement.appendChild(formatHTMLMessage(rawResponse, false, replace, rawQuery));
|
||||||
|
|
||||||
|
// Append loading ellipsis if it exists
|
||||||
|
if (!replace && loadingEllipsis) newResponseElement.appendChild(loadingEllipsis);
|
||||||
|
// Scroll to bottom of chat view
|
||||||
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImageResponse(imageJson, rawResponse) {
|
||||||
|
if (imageJson.image) {
|
||||||
|
const inferredQuery = imageJson.inferredQueries?.[0] ?? "generated image";
|
||||||
|
|
||||||
|
// If response has image field, response is a generated image.
|
||||||
|
if (imageJson.intentType === "text-to-image") {
|
||||||
|
rawResponse += `![generated_image](data:image/png;base64,${imageJson.image})`;
|
||||||
|
} else if (imageJson.intentType === "text-to-image2") {
|
||||||
|
rawResponse += `![generated_image](${imageJson.image})`;
|
||||||
|
} else if (imageJson.intentType === "text-to-image-v3") {
|
||||||
|
rawResponse = `![](data:image/webp;base64,${imageJson.image})`;
|
||||||
|
}
|
||||||
|
if (inferredQuery) {
|
||||||
|
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If response has detail field, response is an error message.
|
||||||
|
if (imageJson.detail) rawResponse += imageJson.detail;
|
||||||
|
|
||||||
|
return rawResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeChatBodyResponse(references, newResponseElement) {
|
||||||
|
if (!!newResponseElement && references != null && Object.keys(references).length > 0) {
|
||||||
|
newResponseElement.appendChild(createReferenceSection(references));
|
||||||
|
}
|
||||||
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
document.getElementById("chat-input")?.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectJsonsInBufferedMessageChunk(chunk) {
|
||||||
|
// Collect list of JSON objects and raw strings in the chunk
|
||||||
|
// Return the list of objects and the remaining raw string
|
||||||
|
let startIndex = chunk.indexOf('{');
|
||||||
|
if (startIndex === -1) return { objects: [chunk], remainder: '' };
|
||||||
|
const objects = [chunk.slice(0, startIndex)];
|
||||||
|
let openBraces = 0;
|
||||||
|
let currentObject = '';
|
||||||
|
|
||||||
|
for (let i = startIndex; i < chunk.length; i++) {
|
||||||
|
if (chunk[i] === '{') {
|
||||||
|
if (openBraces === 0) startIndex = i;
|
||||||
|
openBraces++;
|
||||||
|
}
|
||||||
|
if (chunk[i] === '}') {
|
||||||
|
openBraces--;
|
||||||
|
if (openBraces === 0) {
|
||||||
|
currentObject = chunk.slice(startIndex, i + 1);
|
||||||
|
objects.push(currentObject);
|
||||||
|
currentObject = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
objects: objects,
|
||||||
|
remainder: openBraces > 0 ? chunk.slice(startIndex) : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertMessageChunkToJson(rawChunk) {
|
||||||
|
// Split the chunk into lines
|
||||||
|
if (rawChunk?.startsWith("{") && rawChunk?.endsWith("}")) {
|
||||||
|
try {
|
||||||
|
let jsonChunk = JSON.parse(rawChunk);
|
||||||
|
if (!jsonChunk.type)
|
||||||
|
jsonChunk = {type: 'message', data: jsonChunk};
|
||||||
|
return jsonChunk;
|
||||||
|
} catch (e) {
|
||||||
|
return {type: 'message', data: rawChunk};
|
||||||
|
}
|
||||||
|
} else if (rawChunk.length > 0) {
|
||||||
|
return {type: 'message', data: rawChunk};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMessageChunk(rawChunk) {
|
||||||
|
const chunk = convertMessageChunkToJson(rawChunk);
|
||||||
|
console.debug("Chunk:", chunk);
|
||||||
|
if (!chunk || !chunk.type) return;
|
||||||
|
if (chunk.type ==='status') {
|
||||||
|
console.log(`status: ${chunk.data}`);
|
||||||
|
const statusMessage = chunk.data;
|
||||||
|
handleStreamResponse(chatMessageState.newResponseTextEl, statusMessage, chatMessageState.rawQuery, chatMessageState.loadingEllipsis, false);
|
||||||
|
} else if (chunk.type === 'start_llm_response') {
|
||||||
|
console.log("Started streaming", new Date());
|
||||||
|
} else if (chunk.type === 'end_llm_response') {
|
||||||
|
console.log("Stopped streaming", new Date());
|
||||||
|
|
||||||
|
// Automatically respond with voice if the subscribed user has sent voice message
|
||||||
|
if (chatMessageState.isVoice && "{{ is_active }}" == "True")
|
||||||
|
textToSpeech(chatMessageState.rawResponse);
|
||||||
|
|
||||||
|
// Append any references after all the data has been streamed
|
||||||
|
finalizeChatBodyResponse(chatMessageState.references, chatMessageState.newResponseTextEl);
|
||||||
|
|
||||||
|
const liveQuery = chatMessageState.rawQuery;
|
||||||
|
// Reset variables
|
||||||
|
chatMessageState = {
|
||||||
|
newResponseTextEl: null,
|
||||||
|
newResponseEl: null,
|
||||||
|
loadingEllipsis: null,
|
||||||
|
references: {},
|
||||||
|
rawResponse: "",
|
||||||
|
rawQuery: liveQuery,
|
||||||
|
isVoice: false,
|
||||||
|
}
|
||||||
|
} else if (chunk.type === "references") {
|
||||||
|
chatMessageState.references = {"notes": chunk.data.context, "online": chunk.data.online_results};
|
||||||
|
} else if (chunk.type === 'message') {
|
||||||
|
const chunkData = chunk.data;
|
||||||
|
if (typeof chunkData === 'object' && chunkData !== null) {
|
||||||
|
// If chunkData is already a JSON object
|
||||||
|
handleJsonResponse(chunkData);
|
||||||
|
} else if (typeof chunkData === 'string' && chunkData.trim()?.startsWith("{") && chunkData.trim()?.endsWith("}")) {
|
||||||
|
// Try process chunk data as if it is a JSON object
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(chunkData.trim());
|
||||||
|
handleJsonResponse(jsonData);
|
||||||
|
} catch (e) {
|
||||||
|
chatMessageState.rawResponse += chunkData;
|
||||||
|
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chatMessageState.rawResponse += chunkData;
|
||||||
|
handleStreamResponse(chatMessageState.newResponseTextEl, chatMessageState.rawResponse, chatMessageState.rawQuery, chatMessageState.loadingEllipsis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJsonResponse(jsonData) {
|
||||||
|
if (jsonData.image || jsonData.detail) {
|
||||||
|
chatMessageState.rawResponse = handleImageResponse(jsonData, chatMessageState.rawResponse);
|
||||||
|
} else if (jsonData.response) {
|
||||||
|
chatMessageState.rawResponse = jsonData.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatMessageState.newResponseTextEl) {
|
||||||
|
chatMessageState.newResponseTextEl.innerHTML = "";
|
||||||
|
chatMessageState.newResponseTextEl.appendChild(formatHTMLMessage(chatMessageState.rawResponse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readChatStream(response) {
|
||||||
|
if (!response.body) return;
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buffer = '';
|
||||||
|
let netBracketCount = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
// If the stream is done
|
||||||
|
if (done) {
|
||||||
|
// Process the last chunk
|
||||||
|
processMessageChunk(buffer);
|
||||||
|
buffer = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read chunk from stream and append it to the buffer
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
buffer += chunk;
|
||||||
|
|
||||||
|
// Check if the buffer contains (0 or more) complete JSON objects
|
||||||
|
netBracketCount += (chunk.match(/{/g) || []).length - (chunk.match(/}/g) || []).length;
|
||||||
|
if (netBracketCount === 0) {
|
||||||
|
let chunks = collectJsonsInBufferedMessageChunk(buffer);
|
||||||
|
chunks.objects.forEach((chunk) => processMessageChunk(chunk));
|
||||||
|
buffer = chunks.remainder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -346,7 +346,7 @@
|
||||||
inp.focus();
|
inp.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chat() {
|
async function chat(isVoice=false) {
|
||||||
//set chat body to empty
|
//set chat body to empty
|
||||||
let chatBody = document.getElementById("chat-body");
|
let chatBody = document.getElementById("chat-body");
|
||||||
chatBody.innerHTML = "";
|
chatBody.innerHTML = "";
|
||||||
|
@ -375,9 +375,6 @@
|
||||||
chat_body.dataset.conversationId = conversationID;
|
chat_body.dataset.conversationId = conversationID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate backend API URL to execute query
|
|
||||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
|
|
||||||
|
|
||||||
let newResponseEl = document.createElement("div");
|
let newResponseEl = document.createElement("div");
|
||||||
newResponseEl.classList.add("chat-message", "khoj");
|
newResponseEl.classList.add("chat-message", "khoj");
|
||||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||||
|
@ -388,128 +385,41 @@
|
||||||
newResponseEl.appendChild(newResponseTextEl);
|
newResponseEl.appendChild(newResponseTextEl);
|
||||||
|
|
||||||
// Temporary status message to indicate that Khoj is thinking
|
// Temporary status message to indicate that Khoj is thinking
|
||||||
let loadingEllipsis = document.createElement("div");
|
let loadingEllipsis = createLoadingEllipsis();
|
||||||
loadingEllipsis.classList.add("lds-ellipsis");
|
|
||||||
|
|
||||||
let firstEllipsis = document.createElement("div");
|
|
||||||
firstEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let secondEllipsis = document.createElement("div");
|
|
||||||
secondEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let thirdEllipsis = document.createElement("div");
|
|
||||||
thirdEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
let fourthEllipsis = document.createElement("div");
|
|
||||||
fourthEllipsis.classList.add("lds-ellipsis-item");
|
|
||||||
|
|
||||||
loadingEllipsis.appendChild(firstEllipsis);
|
|
||||||
loadingEllipsis.appendChild(secondEllipsis);
|
|
||||||
loadingEllipsis.appendChild(thirdEllipsis);
|
|
||||||
loadingEllipsis.appendChild(fourthEllipsis);
|
|
||||||
|
|
||||||
newResponseTextEl.appendChild(loadingEllipsis);
|
|
||||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
|
||||||
// Call Khoj chat API
|
|
||||||
let response = await fetch(chatApi, { headers });
|
|
||||||
let rawResponse = "";
|
|
||||||
let references = null;
|
|
||||||
const contentType = response.headers.get("content-type");
|
|
||||||
toggleLoading();
|
toggleLoading();
|
||||||
if (contentType === "application/json") {
|
|
||||||
// Handle JSON response
|
|
||||||
try {
|
|
||||||
const responseAsJson = await response.json();
|
|
||||||
if (responseAsJson.image) {
|
|
||||||
// If response has image field, response is a generated image.
|
|
||||||
if (responseAsJson.intentType === "text-to-image") {
|
|
||||||
rawResponse += `![${query}](data:image/png;base64,${responseAsJson.image})`;
|
|
||||||
} else if (responseAsJson.intentType === "text-to-image2") {
|
|
||||||
rawResponse += `![${query}](${responseAsJson.image})`;
|
|
||||||
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
|
||||||
rawResponse += `![${query}](data:image/webp;base64,${responseAsJson.image})`;
|
|
||||||
}
|
|
||||||
const inferredQueries = responseAsJson.inferredQueries?.[0];
|
|
||||||
if (inferredQueries) {
|
|
||||||
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQueries}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (responseAsJson.context) {
|
|
||||||
const rawReferenceAsJson = responseAsJson.context;
|
|
||||||
references = createReferenceSection(rawReferenceAsJson, createLinkerSection=true);
|
|
||||||
}
|
|
||||||
if (responseAsJson.detail) {
|
|
||||||
// If response has detail field, response is an error message.
|
|
||||||
rawResponse += responseAsJson.detail;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If the chunk is not a JSON object, just display it as is
|
|
||||||
rawResponse += chunk;
|
|
||||||
} finally {
|
|
||||||
newResponseTextEl.innerHTML = "";
|
|
||||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
|
||||||
|
|
||||||
if (references != null) {
|
// Setup chat message state
|
||||||
newResponseTextEl.appendChild(references);
|
chatMessageState = {
|
||||||
}
|
newResponseTextEl,
|
||||||
|
newResponseEl,
|
||||||
|
loadingEllipsis,
|
||||||
|
references: {},
|
||||||
|
rawResponse: "",
|
||||||
|
rawQuery: query,
|
||||||
|
isVoice: isVoice,
|
||||||
|
}
|
||||||
|
|
||||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
// Construct API URL to execute chat query
|
||||||
}
|
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
|
||||||
} else {
|
chatApi += (!!region && !!city && !!countryName && !!timezone)
|
||||||
// Handle streamed response of type text/event-stream or text/plain
|
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||||
const reader = response.body.getReader();
|
: '';
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let references = {};
|
|
||||||
|
|
||||||
readStream();
|
const response = await fetch(chatApi, { headers });
|
||||||
|
|
||||||
function readStream() {
|
try {
|
||||||
reader.read().then(({ done, value }) => {
|
if (!response.ok) throw new Error(response.statusText);
|
||||||
if (done) {
|
if (!response.body) throw new Error("Response body is empty");
|
||||||
// Append any references after all the data has been streamed
|
// Stream and render chat response
|
||||||
if (references != {}) {
|
await readChatStream(response);
|
||||||
newResponseTextEl.appendChild(createReferenceSection(references, createLinkerSection=true));
|
} catch (err) {
|
||||||
}
|
console.error(`Khoj chat response failed with\n${err}`);
|
||||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
||||||
return;
|
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
||||||
}
|
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
||||||
|
newResponseTextEl.textContent = errorMsg;
|
||||||
// Decode message chunk from stream
|
|
||||||
const chunk = decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
if (chunk.includes("### compiled references:")) {
|
|
||||||
const additionalResponse = chunk.split("### compiled references:")[0];
|
|
||||||
rawResponse += additionalResponse;
|
|
||||||
newResponseTextEl.innerHTML = "";
|
|
||||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
|
||||||
|
|
||||||
const rawReference = chunk.split("### compiled references:")[1];
|
|
||||||
const rawReferenceAsJson = JSON.parse(rawReference);
|
|
||||||
if (rawReferenceAsJson instanceof Array) {
|
|
||||||
references["notes"] = rawReferenceAsJson;
|
|
||||||
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
|
||||||
references["online"] = rawReferenceAsJson;
|
|
||||||
}
|
|
||||||
readStream();
|
|
||||||
} else {
|
|
||||||
// Display response from Khoj
|
|
||||||
if (newResponseTextEl.getElementsByClassName("lds-ellipsis").length > 0) {
|
|
||||||
newResponseTextEl.removeChild(loadingEllipsis);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the chunk is not a JSON object, just display it as is
|
|
||||||
rawResponse += chunk;
|
|
||||||
newResponseTextEl.innerHTML = "";
|
|
||||||
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
|
||||||
|
|
||||||
readStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll to bottom of chat window as chat response is streamed
|
|
||||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,8 @@ function toggleNavMenu() {
|
||||||
document.addEventListener('click', function(event) {
|
document.addEventListener('click', function(event) {
|
||||||
let menu = document.getElementById("khoj-nav-menu");
|
let menu = document.getElementById("khoj-nav-menu");
|
||||||
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
||||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
let isClickOnMenu = menuContainer?.contains(event.target) || menuContainer === event.target;
|
||||||
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
if (menu && isClickOnMenu === false && menu.classList.contains("show")) {
|
||||||
menu.classList.remove("show");
|
menu.classList.remove("show");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue