mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Show latest msgs on chat session load. Fetch rest as they near viewport
- Reduces time to first render when loading long chat sessions - Limits size of first page load, when loading long chat sessions These performance improvements are maximally felt for large chat sessions with lots of images generated by Khoj Updated web and desktop app to support these changes for now
This commit is contained in:
parent
9e5585776c
commit
128829c477
2 changed files with 217 additions and 42 deletions
|
@ -130,7 +130,7 @@
|
|||
return referenceButton;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false) {
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") {
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
let formattedMessage = formatHTMLMessage(message, raw);
|
||||
|
@ -153,10 +153,15 @@
|
|||
|
||||
// Append chat message div to chat body
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
if (renderType === "append") {
|
||||
chatBody.appendChild(chatMessage);
|
||||
|
||||
// Scroll to bottom of chat-body element
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
} else if (renderType === "prepend") {
|
||||
chatBody.insertBefore(chatMessage, chatBody.firstChild);
|
||||
} else if (renderType === "return") {
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
|
@ -207,6 +212,7 @@
|
|||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) {
|
||||
// If no document or online context is provided, render the message as is
|
||||
if ((context == null || context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
if (intentType?.includes("text-to-image")) {
|
||||
let imageMarkdown;
|
||||
|
@ -222,24 +228,21 @@
|
|||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
renderMessage(imageMarkdown, by, dt);
|
||||
return;
|
||||
return renderMessage(imageMarkdown, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
renderMessage(message, by, dt);
|
||||
return;
|
||||
return renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
if (context == null && onlineContext == null) {
|
||||
renderMessage(message, by, dt);
|
||||
return;
|
||||
return renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
renderMessage(message, by, dt);
|
||||
return;
|
||||
return renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
// If document or online context is provided, render the message with its references
|
||||
let references = document.createElement('div');
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
|
@ -297,11 +300,10 @@
|
|||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
renderMessage(imageMarkdown, by, dt, references);
|
||||
return;
|
||||
return renderMessage(imageMarkdown, by, dt, references, false, "return");
|
||||
}
|
||||
|
||||
renderMessage(message, by, dt, references);
|
||||
return renderMessage(message, by, dt, references, false, "return");
|
||||
}
|
||||
|
||||
function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) {
|
||||
|
@ -677,7 +679,7 @@
|
|||
let firstRunSetupMessageRendered = false;
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
let chatHistoryUrl = `/api/chat/history?client=desktop`;
|
||||
let chatHistoryUrl = `${hostURL}/api/chat/history?client=desktop`;
|
||||
if (chatBody.dataset.conversationId) {
|
||||
chatHistoryUrl += `&conversation_id=${chatBody.dataset.conversationId}`;
|
||||
}
|
||||
|
@ -689,7 +691,8 @@
|
|||
loadingScreen.appendChild(yellowOrb);
|
||||
chatBody.appendChild(loadingScreen);
|
||||
|
||||
fetch(`${hostURL}${chatHistoryUrl}`, { headers })
|
||||
// Get the most recent 10 chat messages from conversation history
|
||||
fetch(`${chatHistoryUrl}&n=10`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.detail) {
|
||||
|
@ -709,11 +712,21 @@
|
|||
chatBody.dataset.conversationId = response.conversation_id;
|
||||
chatBody.dataset.conversationTitle = response.slug || `New conversation 🌱`;
|
||||
|
||||
const fullChatLog = response.chat || [];
|
||||
// Create a new IntersectionObserver
|
||||
let fetchRemainingMessagesObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
// If the element is in the viewport, fetch the remaining message and unobserve the element
|
||||
if (entry.isIntersecting) {
|
||||
fetchRemainingChatMessages(chatHistoryUrl);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {rootMargin: '0px 0px 0px 0px'});
|
||||
|
||||
fullChatLog.forEach(chat_log => {
|
||||
const fullChatLog = response.chat || [];
|
||||
fullChatLog.forEach((chat_log, index) => {
|
||||
if (chat_log.message != null) {
|
||||
renderMessageWithReference(
|
||||
let messageElement = renderMessageWithReference(
|
||||
chat_log.message,
|
||||
chat_log.by,
|
||||
chat_log.context,
|
||||
|
@ -721,10 +734,25 @@
|
|||
chat_log.onlineContext,
|
||||
chat_log.intent?.type,
|
||||
chat_log.intent?.["inferred-queries"]);
|
||||
chatBody.appendChild(messageElement);
|
||||
|
||||
// When the 4th oldest message is within viewing distance (~60% scrolled up)
|
||||
// Fetch the remaining chat messages
|
||||
if (index === 4) {
|
||||
fetchRemainingMessagesObserver.observe(messageElement);
|
||||
}
|
||||
}
|
||||
loadingScreen.style.height = chatBody.scrollHeight + 'px';
|
||||
})
|
||||
|
||||
// Scroll to bottom of chat-body element
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
|
||||
// Set height of chat-body element to the height of the chat-body-wrapper
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
let chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
chatBody.style.height = chatBodyWrapperHeight;
|
||||
|
||||
// Add fade out animation to loading screen and remove it after the animation ends
|
||||
fadeOutLoadingAnimation(loadingScreen);
|
||||
})
|
||||
|
@ -784,6 +812,65 @@
|
|||
}
|
||||
}
|
||||
|
||||
function fetchRemainingChatMessages(chatHistoryUrl) {
|
||||
// Create a new IntersectionObserver
|
||||
let observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
// If the element is in the viewport, render the message and unobserve the element
|
||||
if (entry.isIntersecting) {
|
||||
let chat_log = entry.target.chat_log;
|
||||
let messageElement = renderMessageWithReference(
|
||||
chat_log.message,
|
||||
chat_log.by,
|
||||
chat_log.context,
|
||||
new Date(chat_log.created),
|
||||
chat_log.onlineContext,
|
||||
chat_log.intent?.type,
|
||||
chat_log.intent?.["inferred-queries"]
|
||||
);
|
||||
entry.target.replaceWith(messageElement);
|
||||
|
||||
// Remove the observer after the element has been rendered
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {rootMargin: '0px 0px 200px 0px'}); // Trigger when the element is within 200px of the viewport
|
||||
|
||||
// Fetch remaining chat messages from conversation history
|
||||
fetch(`${chatHistoryUrl}&n=-10`, { method: "GET" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status != "ok") {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
return data.response;
|
||||
})
|
||||
.then(response => {
|
||||
const fullChatLog = response.chat || [];
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
fullChatLog
|
||||
.reverse()
|
||||
.forEach(chat_log => {
|
||||
if (chat_log.message != null) {
|
||||
// Create a new element for each chat log
|
||||
let placeholder = document.createElement('div');
|
||||
placeholder.chat_log = chat_log;
|
||||
|
||||
// Insert the message placeholder as the first child of chat body after the welcome message
|
||||
chatBody.insertBefore(placeholder, chatBody.firstChild.nextSibling);
|
||||
|
||||
// Observe the element
|
||||
placeholder.style.height = "20px";
|
||||
observer.observe(placeholder);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function fadeOutLoadingAnimation(loadingScreen) {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
|
|
|
@ -160,7 +160,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
return referenceButton;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false) {
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") {
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
let formattedMessage = formatHTMLMessage(message, raw);
|
||||
|
@ -183,10 +183,16 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
|
||||
// Append chat message div to chat body
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
if (renderType === "append") {
|
||||
chatBody.appendChild(chatMessage);
|
||||
|
||||
// Scroll to bottom of chat-body element
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
} else if (renderType === "prepend"){
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.insertBefore(chatMessage, chatBody.firstChild);
|
||||
} else if (renderType === "return") {
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
|
@ -237,6 +243,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) {
|
||||
// If no document or online context is provided, render the message as is
|
||||
if ((context == null || context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
if (intentType?.includes("text-to-image")) {
|
||||
let imageMarkdown;
|
||||
|
@ -251,19 +258,17 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
renderMessage(imageMarkdown, by, dt);
|
||||
return;
|
||||
return renderMessage(imageMarkdown, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
renderMessage(message, by, dt);
|
||||
return;
|
||||
return renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
||||
renderMessage(message, by, dt);
|
||||
return;
|
||||
return renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
// If document or online context is provided, render the message with its references
|
||||
let references = document.createElement('div');
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
|
@ -321,11 +326,10 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
renderMessage(imageMarkdown, by, dt, references);
|
||||
return;
|
||||
return renderMessage(imageMarkdown, by, dt, references, false, "return");
|
||||
}
|
||||
|
||||
renderMessage(message, by, dt, references);
|
||||
return renderMessage(message, by, dt, references, false, "return");
|
||||
}
|
||||
|
||||
function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) {
|
||||
|
@ -1068,7 +1072,8 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
loadingScreen.appendChild(yellowOrb);
|
||||
chatBody.appendChild(loadingScreen);
|
||||
|
||||
fetch(chatHistoryUrl, { method: "GET" })
|
||||
// Get the most recent 10 chat messages from conversation history
|
||||
fetch(`${chatHistoryUrl}&n=10`, { method: "GET" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.detail) {
|
||||
|
@ -1121,11 +1126,22 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
agentMetadataElement.style.display = "none";
|
||||
}
|
||||
|
||||
const fullChatLog = response.chat || [];
|
||||
// Create a new IntersectionObserver
|
||||
let fetchRemainingMessagesObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
// If the element is in the viewport, fetch the remaining message and unobserve the element
|
||||
if (entry.isIntersecting) {
|
||||
fetchRemainingChatMessages(chatHistoryUrl);
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {rootMargin: '0px 0px 0px 0px'});
|
||||
|
||||
fullChatLog.forEach(chat_log => {
|
||||
const fullChatLog = response.chat || [];
|
||||
fullChatLog.forEach((chat_log, index) => {
|
||||
// Render the last 10 messages immediately
|
||||
if (chat_log.message != null) {
|
||||
renderMessageWithReference(
|
||||
let messageElement = renderMessageWithReference(
|
||||
chat_log.message,
|
||||
chat_log.by,
|
||||
chat_log.context,
|
||||
|
@ -1133,14 +1149,26 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
chat_log.onlineContext,
|
||||
chat_log.intent?.type,
|
||||
chat_log.intent?.["inferred-queries"]);
|
||||
chatBody.appendChild(messageElement);
|
||||
|
||||
// When the 4th oldest message is within viewing distance (~60% scroll up)
|
||||
// Fetch the remaining chat messages
|
||||
if (index === 4) {
|
||||
fetchRemainingMessagesObserver.observe(messageElement);
|
||||
}
|
||||
}
|
||||
loadingScreen.style.height = chatBody.scrollHeight + 'px';
|
||||
});
|
||||
|
||||
// Add fade out animation to loading screen and remove it after the animation ends
|
||||
// Scroll to bottom of chat-body element
|
||||
chatBody.scrollTop = chatBody.scrollHeight;
|
||||
|
||||
// Set height of chat-body element to the height of the chat-body-wrapper
|
||||
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
let chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
chatBody.style.height = chatBodyWrapperHeight;
|
||||
|
||||
// Add fade out animation to loading screen and remove it after the animation ends
|
||||
setTimeout(() => {
|
||||
loadingScreen.remove();
|
||||
chatBody.classList.remove("relative-position");
|
||||
|
@ -1198,6 +1226,66 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
document.getElementById("chat-input").value = query_via_url;
|
||||
chat();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function fetchRemainingChatMessages(chatHistoryUrl) {
|
||||
// Create a new IntersectionObserver
|
||||
let observer = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
// If the element is in the viewport, render the message and unobserve the element
|
||||
if (entry.isIntersecting) {
|
||||
let chat_log = entry.target.chat_log;
|
||||
let messageElement = renderMessageWithReference(
|
||||
chat_log.message,
|
||||
chat_log.by,
|
||||
chat_log.context,
|
||||
new Date(chat_log.created),
|
||||
chat_log.onlineContext,
|
||||
chat_log.intent?.type,
|
||||
chat_log.intent?.["inferred-queries"]
|
||||
);
|
||||
entry.target.replaceWith(messageElement);
|
||||
|
||||
// Remove the observer after the element has been rendered
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, {rootMargin: '0px 0px 200px 0px'}); // Trigger when the element is within 200px of the viewport
|
||||
|
||||
// Fetch remaining chat messages from conversation history
|
||||
fetch(`${chatHistoryUrl}&n=-10`, { method: "GET" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status != "ok") {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
return data.response;
|
||||
})
|
||||
.then(response => {
|
||||
const fullChatLog = response.chat || [];
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
fullChatLog
|
||||
.reverse()
|
||||
.forEach(chat_log => {
|
||||
if (chat_log.message != null) {
|
||||
// Create a new element for each chat log
|
||||
let placeholder = document.createElement('div');
|
||||
placeholder.chat_log = chat_log;
|
||||
|
||||
// Insert the message placeholder as the first child of chat body after the welcome message
|
||||
chatBody.insertBefore(placeholder, chatBody.firstChild.nextSibling);
|
||||
|
||||
// Observe the element
|
||||
placeholder.style.height = "20px";
|
||||
observer.observe(placeholder);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function flashStatusInChatInput(message) {
|
||||
|
|
Loading…
Reference in a new issue