mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 13:19:16 +00:00
Add OS Level Shortcut Window for Quick Access to Khoj Desktop (#815)
* rough sketch of desktop shortcuts. many bugs to fix still * working MVP of desktop shortcut khoj * UI fixes * UI improvements for editable shortcut message * major rendering fix to prevent clipboard text from getting lost * UI improvements and bug fixes * UI upgrades: custom top bar, edit sent message and color matching * removed debug javascript file * font reverted to Noto Sans * cleaning up the code and removing diffs * UX fixes * cleaning up unused methods from html * front end for button to send user back to main window to continue conversation * UX fix for window and continue conversation support added * migrated common js functions into chatutils.js * Fix window closing issue in macos by 1. Use a helper function to determine if the window is open by seeing if there's a browser window with shortcut.html loaded 2. Use the event listener on the window to handle teardown * removed extra comment and renamed continue convo button --------- Co-authored-by: sabaimran <narmiabas@gmail.com>
This commit is contained in:
366
src/interface/desktop/chatutils.js
Normal file
366
src/interface/desktop/chatutils.js
Normal file
@@ -0,0 +1,366 @@
|
||||
function copyParentText(event, message=null) { //same
|
||||
const button = event.currentTarget;
|
||||
const textContent = message ?? button.parentNode.textContent.trim();
|
||||
navigator.clipboard.writeText(textContent).then(() => {
|
||||
button.firstChild.src = "./assets/icons/copy-button-success.svg";
|
||||
setTimeout(() => {
|
||||
button.firstChild.src = "./assets/icons/copy-button.svg";
|
||||
}, 1000);
|
||||
}).catch((error) => {
|
||||
console.error("Error copying text to clipboard:", error);
|
||||
const originalButtonText = button.innerHTML;
|
||||
button.innerHTML = "⛔️";
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalButtonText;
|
||||
button.firstChild.src = "./assets/icons/copy-button.svg";
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function createCopyParentText(message) { //same
|
||||
return function(event) {
|
||||
copyParentText(event, message);
|
||||
}
|
||||
}
|
||||
function formatDate(date) { //same
|
||||
// Format date in HH:MM, DD MMM YYYY format
|
||||
let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit'}).replaceAll('-', ' ');
|
||||
return `${time_string}, ${date_string}`;
|
||||
}
|
||||
|
||||
function generateReference(referenceJson, index) { //same
|
||||
let reference = referenceJson.hasOwnProperty("compiled") ? referenceJson.compiled : referenceJson;
|
||||
let referenceFile = referenceJson.hasOwnProperty("file") ? referenceJson.file : null;
|
||||
|
||||
// Escape reference for HTML rendering
|
||||
let escaped_ref = reference.replaceAll('"', '"');
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
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.textContent = 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() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.textContent = escaped_ref;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.textContent = short_ref;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function generateOnlineReference(reference, index) { //same
|
||||
|
||||
// Generate HTML for Chat Reference
|
||||
let title = reference.title || reference.link;
|
||||
let link = reference.link;
|
||||
let snippet = reference.snippet;
|
||||
let question = reference.question;
|
||||
if (question) {
|
||||
question = `<b>Question:</b> ${question}<br><br>`;
|
||||
} else {
|
||||
question = "";
|
||||
}
|
||||
|
||||
let linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', link);
|
||||
linkElement.setAttribute('target', '_blank');
|
||||
linkElement.setAttribute('rel', 'noopener noreferrer');
|
||||
linkElement.classList.add("inline-chat-link");
|
||||
linkElement.classList.add("reference-link");
|
||||
linkElement.setAttribute('title', title);
|
||||
linkElement.textContent = title;
|
||||
|
||||
let referenceButton = document.createElement('button');
|
||||
referenceButton.innerHTML = linkElement.outerHTML;
|
||||
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() {
|
||||
if (this.classList.contains("collapsed")) {
|
||||
this.classList.remove("collapsed");
|
||||
this.classList.add("expanded");
|
||||
this.innerHTML = linkElement.outerHTML + `<br><br>${question + snippet}`;
|
||||
} else {
|
||||
this.classList.add("collapsed");
|
||||
this.classList.remove("expanded");
|
||||
this.innerHTML = linkElement.outerHTML;
|
||||
}
|
||||
});
|
||||
|
||||
return referenceButton;
|
||||
}
|
||||
|
||||
function renderMessage(message, by, dt=null, annotations=null, raw=false, renderType="append") { //same
|
||||
let message_time = formatDate(dt ?? new Date());
|
||||
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
||||
let formattedMessage = formatHTMLMessage(message, raw);
|
||||
|
||||
// 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.appendChild(formattedMessage);
|
||||
chatMessage.appendChild(chatMessageText);
|
||||
|
||||
// Append annotations div to the chat message
|
||||
if (annotations) {
|
||||
chatMessageText.appendChild(annotations);
|
||||
}
|
||||
|
||||
// Append chat message div to chat body
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
let body = document.body;
|
||||
if (renderType === "append") {
|
||||
chatBody.appendChild(chatMessage);
|
||||
// Scroll to bottom of chat-body element
|
||||
body.scrollTop = chatBody.scrollHeight;
|
||||
} else if (renderType === "prepend") {
|
||||
chatBody.insertBefore(chatMessage, chatBody.firstChild);
|
||||
} else if (renderType === "return") {
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
let chatBodyWrapper = document.getElementById("chat-body");
|
||||
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
||||
}
|
||||
|
||||
function processOnlineReferences(referenceSection, onlineContext) { //same
|
||||
let numOnlineReferences = 0;
|
||||
for (let subquery in onlineContext) {
|
||||
let onlineReference = onlineContext[subquery];
|
||||
if (onlineReference.organic && onlineReference.organic.length > 0) {
|
||||
numOnlineReferences += onlineReference.organic.length;
|
||||
for (let index in onlineReference.organic) {
|
||||
let reference = onlineReference.organic[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.knowledgeGraph && onlineReference.knowledgeGraph.length > 0) {
|
||||
numOnlineReferences += onlineReference.knowledgeGraph.length;
|
||||
for (let index in onlineReference.knowledgeGraph) {
|
||||
let reference = onlineReference.knowledgeGraph[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.peopleAlsoAsk && onlineReference.peopleAlsoAsk.length > 0) {
|
||||
numOnlineReferences += onlineReference.peopleAlsoAsk.length;
|
||||
for (let index in onlineReference.peopleAlsoAsk) {
|
||||
let reference = onlineReference.peopleAlsoAsk[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlineReference.webpages && onlineReference.webpages.length > 0) {
|
||||
numOnlineReferences += onlineReference.webpages.length;
|
||||
for (let index in onlineReference.webpages) {
|
||||
let reference = onlineReference.webpages[index];
|
||||
let polishedReference = generateOnlineReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numOnlineReferences;
|
||||
}
|
||||
|
||||
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) { //same
|
||||
let chatEl;
|
||||
if (intentType?.includes("text-to-image")) {
|
||||
let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries);
|
||||
chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return");
|
||||
} else {
|
||||
chatEl = renderMessage(message, by, dt, null, false, "return");
|
||||
}
|
||||
|
||||
// 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))) {
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
// If document or online context is provided, render the message with its references
|
||||
let references = {};
|
||||
if (!!context) references["notes"] = context;
|
||||
if (!!onlineContext) references["online"] = onlineContext;
|
||||
let chatMessageEl = chatEl.getElementsByClassName("chat-message-text")[0];
|
||||
chatMessageEl.appendChild(createReferenceSection(references));
|
||||
|
||||
return chatEl;
|
||||
}
|
||||
|
||||
function generateImageMarkdown(message, intentType, inferredQueries=null) { //same
|
||||
let imageMarkdown;
|
||||
if (intentType === "text-to-image") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image2") {
|
||||
imageMarkdown = ``;
|
||||
} else if (intentType === "text-to-image-v3") {
|
||||
imageMarkdown = ``;
|
||||
}
|
||||
const inferredQuery = inferredQueries?.[0];
|
||||
if (inferredQuery) {
|
||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
||||
}
|
||||
return imageMarkdown;
|
||||
}
|
||||
|
||||
function formatHTMLMessage(message, raw=false, willReplace=true) { //same
|
||||
var md = window.markdownit();
|
||||
let newHTML = message;
|
||||
|
||||
// Remove any text between <s>[INST] and </s> tags. These are spurious instructions for the AI chat model.
|
||||
newHTML = newHTML.replace(/<s>\[INST\].+(<\/s>)?/g, '');
|
||||
|
||||
// Customize the rendering of images
|
||||
md.renderer.rules.image = function(tokens, idx, options, env, self) {
|
||||
let token = tokens[idx];
|
||||
|
||||
// Add class="text-to-image" to images
|
||||
token.attrPush(['class', 'text-to-image']);
|
||||
|
||||
// Use the default renderer to render image markdown format
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
|
||||
// Render markdown
|
||||
newHTML = raw ? newHTML : md.render(newHTML);
|
||||
// Sanitize the rendered markdown
|
||||
newHTML = DOMPurify.sanitize(newHTML);
|
||||
// Set rendered markdown to HTML DOM element
|
||||
let element = document.createElement('div');
|
||||
element.innerHTML = newHTML;
|
||||
element.className = "chat-message-text-response";
|
||||
|
||||
// Add a copy button to each chat message
|
||||
if (willReplace === true) {
|
||||
let copyButton = document.createElement('button');
|
||||
copyButton.classList.add("copy-button");
|
||||
copyButton.title = "Copy Message";
|
||||
let copyIcon = document.createElement("img");
|
||||
copyIcon.id = "copy-icon";
|
||||
copyIcon.src = "./assets/icons/copy-button.svg";
|
||||
copyIcon.classList.add("copy-icon");
|
||||
copyButton.appendChild(copyIcon);
|
||||
copyButton.addEventListener('click', createCopyParentText(message));
|
||||
element.append(copyButton);
|
||||
}
|
||||
|
||||
// Get any elements with a class that starts with "language"
|
||||
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
|
||||
// For each element, add a parent div with the class "programmatic-output"
|
||||
codeBlockElements.forEach((codeElement, key) => {
|
||||
// Create the parent div
|
||||
let parentDiv = document.createElement('div');
|
||||
parentDiv.classList.add("programmatic-output");
|
||||
// Add the parent div before the code element
|
||||
codeElement.parentNode.insertBefore(parentDiv, codeElement);
|
||||
// Move the code element into the parent div
|
||||
parentDiv.appendChild(codeElement);
|
||||
// Add a copy button to each element
|
||||
});
|
||||
|
||||
// Get all code elements that have no class.
|
||||
let codeElements = element.querySelectorAll('code:not([class])');
|
||||
codeElements.forEach((codeElement) => {
|
||||
// Add the class "chat-response" to each element
|
||||
codeElement.classList.add("chat-response");
|
||||
});
|
||||
|
||||
let anchorElements = element.querySelectorAll('a');
|
||||
anchorElements.forEach((anchorElement) => {
|
||||
// Tag external links to open in separate window
|
||||
if (
|
||||
!anchorElement.href.startsWith("./") &&
|
||||
!anchorElement.href.startsWith("#") &&
|
||||
!anchorElement.href.startsWith("/")
|
||||
) {
|
||||
anchorElement.setAttribute('target', '_blank');
|
||||
anchorElement.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
|
||||
// Add the class "inline-chat-link" to each element
|
||||
anchorElement.classList.add("inline-chat-link");
|
||||
});
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
function createReferenceSection(references, createLinkerSection=false) {
|
||||
console.log("linker data: ", createLinkerSection);
|
||||
let referenceSection = document.createElement('div');
|
||||
referenceSection.classList.add("reference-section");
|
||||
referenceSection.classList.add("collapsed");
|
||||
|
||||
let numReferences = 0;
|
||||
|
||||
if (references.hasOwnProperty("notes")) {
|
||||
numReferences += references["notes"].length;
|
||||
|
||||
references["notes"].forEach((reference, index) => {
|
||||
let polishedReference = generateReference(reference, index);
|
||||
referenceSection.appendChild(polishedReference);
|
||||
});
|
||||
}
|
||||
if (references.hasOwnProperty("online")){
|
||||
numReferences += processOnlineReferences(referenceSection, references["online"]);
|
||||
}
|
||||
|
||||
let referenceExpandButton = document.createElement('button');
|
||||
referenceExpandButton.id = "reference-expand-button";
|
||||
referenceExpandButton.classList.add("reference-expand-button");
|
||||
referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
|
||||
let referencesDiv = document.createElement('div');
|
||||
referencesDiv.classList.add("references");
|
||||
referencesDiv.appendChild(referenceExpandButton);
|
||||
if (createLinkerSection) {
|
||||
//add a linker button back to the desktop application
|
||||
let linkerButton = document.createElement('button');
|
||||
linkerButton.innerHTML = "Continue Conversation";
|
||||
linkerButton.id = "linker-button";
|
||||
linkerButton.addEventListener('click', function() {
|
||||
window.routeBackToMainWindowAPI.sendSignal();
|
||||
});
|
||||
referencesDiv.appendChild(linkerButton);
|
||||
console.log("shortcut window");
|
||||
}
|
||||
referencesDiv.appendChild(referenceSection);
|
||||
|
||||
return referencesDiv;
|
||||
}
|
||||
Reference in New Issue
Block a user