From 270f7b3eb30a65e5093a00cbbab0dd863e322ca2 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 15:46:43 -0800 Subject: [PATCH 1/3] Update the chat UI to have richer representation of the references --- src/khoj/interface/web/chat.html | 214 ++++++++++++++++++++++++++++--- 1 file changed, 194 insertions(+), 20 deletions(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 838155a5..dc3b70ee 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -33,32 +33,101 @@ let escaped_ref = reference.replaceAll('"', '"'); // Generate HTML for Chat Reference - return `${index}`; + let short_ref = escaped_ref.slice(0, 100); + short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref; + let referenceButton = document.createElement('button'); + referenceButton.innerHTML = short_ref; + referenceButton.id = `ref-${index}`; + referenceButton.classList.add("reference-button"); + referenceButton.classList.add("collapsed"); + referenceButton.tabIndex = 0; + + // Add event listener to toggle full reference on click + referenceButton.addEventListener('click', function() { + console.log(`Toggling ref-${index}`) + if (this.classList.contains("collapsed")) { + this.classList.remove("collapsed"); + this.classList.add("expanded"); + this.innerHTML = escaped_ref; + } else { + this.classList.add("collapsed"); + this.classList.remove("expanded"); + this.innerHTML = short_ref; + } + }); + + return referenceButton; } - function renderMessage(message, by, dt=null) { + function renderMessage(message, by, dt=null, annotations=null) { let message_time = formatDate(dt ?? new Date()); let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let formattedMessage = formatHTMLMessage(message); - // Generate HTML for Chat Message and Append to Chat Body - document.getElementById("chat-body").innerHTML += ` -
-
${formattedMessage}
-
- `; + let chatBody = document.getElementById("chat-body"); + + // Create a new div for the chat message + let chatMessage = document.createElement('div'); + chatMessage.className = `chat-message ${by}`; + chatMessage.dataset.meta = `${by_name} at ${message_time}`; + + // Create a new div for the chat message text and append it to the chat message + let chatMessageText = document.createElement('div'); + chatMessageText.className = `chat-message-text ${by}`; + chatMessageText.innerHTML = formattedMessage; + chatMessage.appendChild(chatMessageText); + + // Append annotations div to the chat message + if (annotations) { + chatMessageText.appendChild(annotations); + } + + // Append chat message div to chat body + chatBody.appendChild(chatMessage); + // Scroll to bottom of chat-body element - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + chatBody.scrollTop = chatBody.scrollHeight; } function renderMessageWithReference(message, by, context=null, dt=null) { - let references = ''; - if (context) { - references = context - .map((reference, index) => generateReference(reference, index)) - .join(","); + if (context == null || context.length == 0) { + renderMessage(message, by, dt); + return; } - renderMessage(message+references, by, dt); + let references = document.createElement('div'); + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = context.length == 1 ? "1 reference" : `${context.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + references.classList.add("references"); + if (context) { + for (let index in context) { + let reference = context[index]; + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + } + } + references.appendChild(referenceSection); + + renderMessage(message, by, dt, references); } function formatHTMLMessage(htmlMessage) { @@ -120,12 +189,14 @@ const decoder = new TextDecoder(); function readStream() { + let reference = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed const currentHTML = newResponseText.innerHTML; newResponseText.innerHTML = formatHTMLMessage(currentHTML); - + newResponseText.appendChild(references); + document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; return; } @@ -138,11 +209,36 @@ const rawReference = chunk.split("### compiled references:")[1]; const rawReferenceAsJson = JSON.parse(rawReference); - let polishedReference = rawReferenceAsJson.map((reference, index) => generateReference(reference, index)) - .join(","); + references = document.createElement('div'); + references.classList.add("references"); - newResponseText.innerHTML += polishedReference; - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = rawReferenceAsJson.length == 1 ? "1 reference" : `${rawReferenceAsJson.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + rawReferenceAsJson.forEach((reference, index) => { + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + }); + references.appendChild(referenceSection); readStream(); } else { // Display response from Khoj @@ -237,6 +333,7 @@ }); }) .catch(err => { + console.log(err); return; }); @@ -299,6 +396,83 @@ padding: 10px; margin: 10px; } + + div.collapsed { + display: none; + } + + div.expanded { + display: block; + } + + div.reference { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + div.expanded.reference-section { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + button.reference-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.2s ease-in-out; + text-align: left; + max-height: 50px; + transition: max-height 0.3s ease-in-out; + overflow: hidden; + } + button.reference-button.expanded { + max-height: 200px; + } + + button.reference-button::before { + content: "▶"; + margin-right: 5px; + display: inline-block; + transition: transform 0.3s ease-in-out; + } + + button.reference-button:active:before, + button.reference-button[aria-expanded="true"]::before { + transform: rotate(90deg); + } + + button.reference-expand-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px dotted var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.4s ease-in-out; + text-align: left; + } + + button.reference-expand-button:hover { + background: var(--primary-hover); + } + #chat-body { font-size: medium; margin: 0px; From e01ecf141962452939d12dd771288ab31564aeb6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 6 Nov 2023 16:12:25 -0800 Subject: [PATCH 2/3] /s/references/reference to fix bug of jumping references --- src/khoj/interface/web/chat.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index dc3b70ee..61f176c7 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -189,7 +189,7 @@ const decoder = new TextDecoder(); function readStream() { - let reference = null; + let references = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed From 6c8689e4aed558358ce7230a02d6c76b557c3091 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 6 Nov 2023 16:18:41 -0800 Subject: [PATCH 3/3] Update corresponding chat UX in the desktop client as well --- src/interface/desktop/chat.html | 215 ++++++++++++++++++++++++++++---- 1 file changed, 194 insertions(+), 21 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index b908b747..8666b340 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -34,32 +34,101 @@ let escaped_ref = reference.replaceAll('"', '"'); // Generate HTML for Chat Reference - return `${index}`; + let short_ref = escaped_ref.slice(0, 100); + short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref; + let referenceButton = document.createElement('button'); + referenceButton.innerHTML = short_ref; + referenceButton.id = `ref-${index}`; + referenceButton.classList.add("reference-button"); + referenceButton.classList.add("collapsed"); + referenceButton.tabIndex = 0; + + // Add event listener to toggle full reference on click + referenceButton.addEventListener('click', function() { + console.log(`Toggling ref-${index}`) + if (this.classList.contains("collapsed")) { + this.classList.remove("collapsed"); + this.classList.add("expanded"); + this.innerHTML = escaped_ref; + } else { + this.classList.add("collapsed"); + this.classList.remove("expanded"); + this.innerHTML = short_ref; + } + }); + + return referenceButton; } - function renderMessage(message, by, dt=null) { + function renderMessage(message, by, dt=null, annotations=null) { let message_time = formatDate(dt ?? new Date()); let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let formattedMessage = formatHTMLMessage(message); - // Generate HTML for Chat Message and Append to Chat Body - document.getElementById("chat-body").innerHTML += ` -
-
${formattedMessage}
-
- `; + let chatBody = document.getElementById("chat-body"); + + // Create a new div for the chat message + let chatMessage = document.createElement('div'); + chatMessage.className = `chat-message ${by}`; + chatMessage.dataset.meta = `${by_name} at ${message_time}`; + + // Create a new div for the chat message text and append it to the chat message + let chatMessageText = document.createElement('div'); + chatMessageText.className = `chat-message-text ${by}`; + chatMessageText.innerHTML = formattedMessage; + chatMessage.appendChild(chatMessageText); + + // Append annotations div to the chat message + if (annotations) { + chatMessageText.appendChild(annotations); + } + + // Append chat message div to chat body + chatBody.appendChild(chatMessage); + // Scroll to bottom of chat-body element - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + chatBody.scrollTop = chatBody.scrollHeight; } function renderMessageWithReference(message, by, context=null, dt=null) { - let references = ''; - if (context) { - references = context - .map((reference, index) => generateReference(reference, index)) - .join(","); + if (context == null || context.length == 0) { + renderMessage(message, by, dt); + return; } - renderMessage(message+references, by, dt); + let references = document.createElement('div'); + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = context.length == 1 ? "1 reference" : `${context.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + references.classList.add("references"); + if (context) { + for (let index in context) { + let reference = context[index]; + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + } + } + references.appendChild(referenceSection); + + renderMessage(message, by, dt, references); } function formatHTMLMessage(htmlMessage) { @@ -120,17 +189,19 @@ // Call specified Khoj API which returns a streamed response of type text/plain fetch(url, { headers }) - .then(response => { + .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); function readStream() { + let references = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed const currentHTML = newResponseText.innerHTML; newResponseText.innerHTML = formatHTMLMessage(currentHTML); - + newResponseText.appendChild(references); + document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; return; } @@ -143,11 +214,36 @@ const rawReference = chunk.split("### compiled references:")[1]; const rawReferenceAsJson = JSON.parse(rawReference); - let polishedReference = rawReferenceAsJson.map((reference, index) => generateReference(reference, index)) - .join(","); + references = document.createElement('div'); + references.classList.add("references"); - newResponseText.innerHTML += polishedReference; - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = rawReferenceAsJson.length == 1 ? "1 reference" : `${rawReferenceAsJson.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + rawReferenceAsJson.forEach((reference, index) => { + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + }); + references.appendChild(referenceSection); readStream(); } else { // Display response from Khoj @@ -443,6 +539,83 @@ box-shadow: 0 0 12px rgb(119, 156, 46); } + div.collapsed { + display: none; + } + + div.expanded { + display: block; + } + + div.reference { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + div.expanded.reference-section { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + button.reference-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.2s ease-in-out; + text-align: left; + max-height: 50px; + transition: max-height 0.3s ease-in-out; + overflow: hidden; + } + button.reference-button.expanded { + max-height: 200px; + } + + button.reference-button::before { + content: "▶"; + margin-right: 5px; + display: inline-block; + transition: transform 0.3s ease-in-out; + } + + button.reference-button:active:before, + button.reference-button[aria-expanded="true"]::before { + transform: rotate(90deg); + } + + button.reference-expand-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px dotted var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.4s ease-in-out; + text-align: left; + } + + button.reference-expand-button:hover { + background: var(--primary-hover); + } + + .option-enabled:focus { outline: none !important; border:1px solid #475569;