diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html
index 3550799e..57657ef1 100644
--- a/src/interface/desktop/chat.html
+++ b/src/interface/desktop/chat.html
@@ -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 += ``;
- } 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 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) {
if (!event.shiftKey && event.key === 'Enter') {
event.preventDefault();
diff --git a/src/interface/desktop/chatutils.js b/src/interface/desktop/chatutils.js
index 42cfa986..84f5e431 100644
--- a/src/interface/desktop/chatutils.js
+++ b/src/interface/desktop/chatutils.js
@@ -364,3 +364,219 @@ 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 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;
+ }
+ }
+}
diff --git a/src/interface/desktop/shortcut.html b/src/interface/desktop/shortcut.html
index 4af26f0d..52207f20 100644
--- a/src/interface/desktop/shortcut.html
+++ b/src/interface/desktop/shortcut.html
@@ -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 team@khoj.dev or on Discord";
+ newResponseTextEl.textContent = errorMsg;
}
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
}
diff --git a/src/interface/desktop/utils.js b/src/interface/desktop/utils.js
index c880a7cd..af0234ea 100644
--- a/src/interface/desktop/utils.js
+++ b/src/interface/desktop/utils.js
@@ -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");
}
});