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; 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 message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw); let formattedMessage = formatHTMLMessage(message, raw);
@ -153,10 +153,15 @@
// Append chat message div to chat body // Append chat message div to chat body
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
if (renderType === "append") {
chatBody.appendChild(chatMessage); chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element // Scroll to bottom of chat-body element
chatBody.scrollTop = chatBody.scrollHeight; 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"); let chatBodyWrapper = document.getElementById("chat-body-wrapper");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight; chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
@ -207,6 +212,7 @@
} }
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) { 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 ((context == null || context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
if (intentType?.includes("text-to-image")) { if (intentType?.includes("text-to-image")) {
let imageMarkdown; let imageMarkdown;
@ -222,24 +228,21 @@
if (inferredQuery) { if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`; imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
} }
renderMessage(imageMarkdown, by, dt); return renderMessage(imageMarkdown, by, dt, null, false, "return");
return;
} }
renderMessage(message, by, dt); return renderMessage(message, by, dt, null, false, "return");
return;
} }
if (context == null && onlineContext == null) { if (context == null && onlineContext == null) {
renderMessage(message, by, dt); return renderMessage(message, by, dt, null, false, "return");
return;
} }
if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) { if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
renderMessage(message, by, dt); return renderMessage(message, by, dt, null, false, "return");
return;
} }
// If document or online context is provided, render the message with its references
let references = document.createElement('div'); let references = document.createElement('div');
let referenceExpandButton = document.createElement('button'); let referenceExpandButton = document.createElement('button');
@ -297,11 +300,10 @@
if (inferredQuery) { if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`; imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
} }
renderMessage(imageMarkdown, by, dt, references); return renderMessage(imageMarkdown, by, dt, references, false, "return");
return;
} }
renderMessage(message, by, dt, references); return renderMessage(message, by, dt, references, false, "return");
} }
function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) { function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) {
@ -677,7 +679,7 @@
let firstRunSetupMessageRendered = false; let firstRunSetupMessageRendered = false;
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
chatBody.innerHTML = ""; chatBody.innerHTML = "";
let chatHistoryUrl = `/api/chat/history?client=desktop`; let chatHistoryUrl = `${hostURL}/api/chat/history?client=desktop`;
if (chatBody.dataset.conversationId) { if (chatBody.dataset.conversationId) {
chatHistoryUrl += `&conversation_id=${chatBody.dataset.conversationId}`; chatHistoryUrl += `&conversation_id=${chatBody.dataset.conversationId}`;
} }
@ -689,7 +691,8 @@
loadingScreen.appendChild(yellowOrb); loadingScreen.appendChild(yellowOrb);
chatBody.appendChild(loadingScreen); 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(response => response.json())
.then(data => { .then(data => {
if (data.detail) { if (data.detail) {
@ -709,11 +712,21 @@
chatBody.dataset.conversationId = response.conversation_id; chatBody.dataset.conversationId = response.conversation_id;
chatBody.dataset.conversationTitle = response.slug || `New conversation 🌱`; 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) { if (chat_log.message != null) {
renderMessageWithReference( let messageElement = renderMessageWithReference(
chat_log.message, chat_log.message,
chat_log.by, chat_log.by,
chat_log.context, chat_log.context,
@ -721,10 +734,25 @@
chat_log.onlineContext, chat_log.onlineContext,
chat_log.intent?.type, chat_log.intent?.type,
chat_log.intent?.["inferred-queries"]); 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'; 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 // Add fade out animation to loading screen and remove it after the animation ends
fadeOutLoadingAnimation(loadingScreen); 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) { function fadeOutLoadingAnimation(loadingScreen) {
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
let chatBodyWrapper = document.getElementById("chat-body-wrapper"); 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; 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 message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
let formattedMessage = formatHTMLMessage(message, raw); 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 // Append chat message div to chat body
let chatBody = document.getElementById("chat-body"); let chatBody = document.getElementById("chat-body");
if (renderType === "append") {
chatBody.appendChild(chatMessage); chatBody.appendChild(chatMessage);
// Scroll to bottom of chat-body element // Scroll to bottom of chat-body element
chatBody.scrollTop = chatBody.scrollHeight; 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"); let chatBodyWrapper = document.getElementById("chat-body-wrapper");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight; 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) { 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 ((context == null || context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
if (intentType?.includes("text-to-image")) { if (intentType?.includes("text-to-image")) {
let imageMarkdown; 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) { if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`; imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
} }
renderMessage(imageMarkdown, by, dt); return renderMessage(imageMarkdown, by, dt, null, false, "return");
return;
} }
renderMessage(message, by, dt); return renderMessage(message, by, dt, null, false, "return");
return;
} }
if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) { if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
renderMessage(message, by, dt); return renderMessage(message, by, dt, null, false, "return");
return;
} }
// If document or online context is provided, render the message with its references
let references = document.createElement('div'); let references = document.createElement('div');
let referenceExpandButton = document.createElement('button'); 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) { if (inferredQuery) {
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`; imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
} }
renderMessage(imageMarkdown, by, dt, references); return renderMessage(imageMarkdown, by, dt, references, false, "return");
return;
} }
renderMessage(message, by, dt, references); return renderMessage(message, by, dt, references, false, "return");
} }
function formatHTMLMessage(htmlMessage, raw=false, willReplace=true) { 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); loadingScreen.appendChild(yellowOrb);
chatBody.appendChild(loadingScreen); 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(response => response.json())
.then(data => { .then(data => {
if (data.detail) { 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"; 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) { if (chat_log.message != null) {
renderMessageWithReference( let messageElement = renderMessageWithReference(
chat_log.message, chat_log.message,
chat_log.by, chat_log.by,
chat_log.context, 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.onlineContext,
chat_log.intent?.type, chat_log.intent?.type,
chat_log.intent?.["inferred-queries"]); 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'; 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"); let chatBodyWrapper = document.getElementById("chat-body-wrapper");
chatBodyWrapperHeight = chatBodyWrapper.clientHeight; let chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
chatBody.style.height = chatBodyWrapperHeight; chatBody.style.height = chatBodyWrapperHeight;
// Add fade out animation to loading screen and remove it after the animation ends
setTimeout(() => { setTimeout(() => {
loadingScreen.remove(); loadingScreen.remove();
chatBody.classList.remove("relative-position"); 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; document.getElementById("chat-input").value = query_via_url;
chat(); 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) { function flashStatusInChatInput(message) {