diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html
index c5b25318..fc7ecc2e 100644
--- a/src/interface/desktop/chat.html
+++ b/src/interface/desktop/chat.html
@@ -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");
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html
index 79df4f2c..81946fa7 100644
--- a/src/khoj/interface/web/chat.html
+++ b/src/khoj/interface/web/chat.html
@@ -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) {