mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-04 13:20:17 +00:00
Merge branch 'master' of github.com:khoj-ai/khoj into features/big-upgrade-chat-ux
This commit is contained in:
@@ -61,6 +61,14 @@
|
||||
let city = null;
|
||||
let countryName = null;
|
||||
let timezone = null;
|
||||
let chatMessageState = {
|
||||
newResponseTextEl: null,
|
||||
newResponseEl: null,
|
||||
loadingEllipsis: null,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
isVoice: false,
|
||||
}
|
||||
|
||||
fetch("https://ipapi.co/json")
|
||||
.then(response => response.json())
|
||||
@@ -75,10 +83,9 @@
|
||||
return;
|
||||
});
|
||||
|
||||
async function chat() {
|
||||
// Extract required fields for search from form
|
||||
async function chat(isVoice=false) {
|
||||
// Extract chat message from chat input form
|
||||
let query = document.getElementById("chat-input").value.trim();
|
||||
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
||||
console.log(`Query: ${query}`);
|
||||
|
||||
// Short circuit on empty query
|
||||
@@ -106,9 +113,6 @@
|
||||
await refreshChatSessionsPanel();
|
||||
}
|
||||
|
||||
// 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");
|
||||
newResponseEl.classList.add("chat-message", "khoj");
|
||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||
@@ -119,25 +123,7 @@
|
||||
newResponseEl.appendChild(newResponseTextEl);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
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);
|
||||
let loadingEllipsis = createLoadingEllipsis();
|
||||
|
||||
newResponseTextEl.appendChild(loadingEllipsis);
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
@@ -148,107 +134,36 @@
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
chatInput.classList.remove("option-enabled");
|
||||
|
||||
// Setup chat message state
|
||||
chatMessageState = {
|
||||
newResponseTextEl,
|
||||
newResponseEl,
|
||||
loadingEllipsis,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: query,
|
||||
isVoice: isVoice,
|
||||
}
|
||||
|
||||
// Call Khoj chat API
|
||||
let response = await fetch(chatApi, { headers });
|
||||
let rawResponse = "";
|
||||
let references = null;
|
||||
const contentType = response.headers.get("content-type");
|
||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
|
||||
chatApi += (!!region && !!city && !!countryName && !!timezone)
|
||||
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||
: '';
|
||||
|
||||
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 += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image2") {
|
||||
rawResponse += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
||||
rawResponse += ``;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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));
|
||||
const response = await fetch(chatApi, { headers });
|
||||
|
||||
if (references != null) {
|
||||
newResponseTextEl.appendChild(references);
|
||||
}
|
||||
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
document.getElementById("chat-input").removeAttribute("disabled");
|
||||
}
|
||||
} else {
|
||||
// Handle streamed response of type text/event-stream or text/plain
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let references = {};
|
||||
|
||||
readStream();
|
||||
|
||||
function readStream() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
// Append any references after all the data has been streamed
|
||||
if (references != {}) {
|
||||
newResponseTextEl.appendChild(createReferenceSection(references));
|
||||
}
|
||||
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
document.getElementById("chat-input").removeAttribute("disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is empty");
|
||||
// Stream and render chat response
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
console.error(`Khoj chat response failed with\n${err}`);
|
||||
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -364,3 +364,194 @@ function createReferenceSection(references, createLinkerSection=false) {
|
||||
|
||||
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 += ``;
|
||||
} else if (imageJson.intentType === "text-to-image2") {
|
||||
rawResponse += ``;
|
||||
} else if (imageJson.intentType === "text-to-image-v3") {
|
||||
rawResponse = ``;
|
||||
}
|
||||
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 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.onlineContext};
|
||||
} 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();
|
||||
const eventDelimiter = '␃🔚␗';
|
||||
let buffer = '';
|
||||
|
||||
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 });
|
||||
console.debug("Raw Chunk:", chunk)
|
||||
// Start buffering chunks until complete event is received
|
||||
buffer += chunk;
|
||||
|
||||
// Once the buffer contains a complete event
|
||||
let newEventIndex;
|
||||
while ((newEventIndex = buffer.indexOf(eventDelimiter)) !== -1) {
|
||||
// Extract the event from the buffer
|
||||
const event = buffer.slice(0, newEventIndex);
|
||||
buffer = buffer.slice(newEventIndex + eventDelimiter.length);
|
||||
|
||||
// Process the event
|
||||
if (event) processMessageChunk(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Khoj",
|
||||
"version": "1.16.0",
|
||||
"version": "1.17.0",
|
||||
"description": "An AI copilot for your Second Brain",
|
||||
"author": "Saba Imran, Debanjum Singh Solanky <team@khoj.dev>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
inp.focus();
|
||||
}
|
||||
|
||||
async function chat() {
|
||||
async function chat(isVoice=false) {
|
||||
//set chat body to empty
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
@@ -375,9 +375,6 @@
|
||||
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");
|
||||
newResponseEl.classList.add("chat-message", "khoj");
|
||||
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||
@@ -388,128 +385,41 @@
|
||||
newResponseEl.appendChild(newResponseTextEl);
|
||||
|
||||
// Temporary status message to indicate that Khoj is thinking
|
||||
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);
|
||||
|
||||
newResponseTextEl.appendChild(loadingEllipsis);
|
||||
let loadingEllipsis = createLoadingEllipsis();
|
||||
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();
|
||||
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 += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image2") {
|
||||
rawResponse += ``;
|
||||
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
||||
rawResponse += ``;
|
||||
}
|
||||
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) {
|
||||
newResponseTextEl.appendChild(references);
|
||||
}
|
||||
// Setup chat message state
|
||||
chatMessageState = {
|
||||
newResponseTextEl,
|
||||
newResponseEl,
|
||||
loadingEllipsis,
|
||||
references: {},
|
||||
rawResponse: "",
|
||||
rawQuery: query,
|
||||
isVoice: isVoice,
|
||||
}
|
||||
|
||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
} else {
|
||||
// Handle streamed response of type text/event-stream or text/plain
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let references = {};
|
||||
// Construct API URL to execute chat query
|
||||
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`;
|
||||
chatApi += (!!region && !!city && !!countryName && !!timezone)
|
||||
? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`
|
||||
: '';
|
||||
|
||||
readStream();
|
||||
const response = await fetch(chatApi, { headers });
|
||||
|
||||
function readStream() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
// Append any references after all the data has been streamed
|
||||
if (references != {}) {
|
||||
newResponseTextEl.appendChild(createReferenceSection(references, createLinkerSection=true));
|
||||
}
|
||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
if (!response.body) throw new Error("Response body is empty");
|
||||
// Stream and render chat response
|
||||
await readChatStream(response);
|
||||
} catch (err) {
|
||||
console.error(`Khoj chat response failed with\n${err}`);
|
||||
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
||||
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;
|
||||
}
|
||||
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ function toggleNavMenu() {
|
||||
document.addEventListener('click', function(event) {
|
||||
let menu = document.getElementById("khoj-nav-menu");
|
||||
let menuContainer = document.getElementById("khoj-nav-menu-container");
|
||||
let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target;
|
||||
if (isClickOnMenu === false && menu.classList.contains("show")) {
|
||||
let isClickOnMenu = menuContainer?.contains(event.target) || menuContainer === event.target;
|
||||
if (menu && isClickOnMenu === false && menu.classList.contains("show")) {
|
||||
menu.classList.remove("show");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user