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:
Debanjum Singh Solanky 2024-04-15 13:44:01 +05:30
parent 9e5585776c
commit 128829c477
2 changed files with 217 additions and 42 deletions

View file

@ -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");
chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element
chatBody.scrollTop = chatBody.scrollHeight;
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");

View file

@ -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");
chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element
chatBody.scrollTop = chatBody.scrollHeight;
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 => {
if (chat_log.message != null){
renderMessageWithReference(
const fullChatLog = response.chat || [];
fullChatLog.forEach((chat_log, index) => {
// Render the last 10 messages immediately
if (chat_log.message != null) {
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) {