mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-30 10:53:02 +01: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:
parent
870d9ecdbf
commit
24a0d8b073
5 changed files with 981 additions and 351 deletions
|
@ -13,7 +13,7 @@
|
||||||
<link rel="stylesheet" href="https://assets.khoj.dev/higlightjs/solarized-dark.min.css">
|
<link rel="stylesheet" href="https://assets.khoj.dev/higlightjs/solarized-dark.min.css">
|
||||||
<script src="https://assets.khoj.dev/higlightjs/highlight.min.js"></script>
|
<script src="https://assets.khoj.dev/higlightjs/highlight.min.js"></script>
|
||||||
<script src="./utils.js"></script>
|
<script src="./utils.js"></script>
|
||||||
|
<script src="chatutils.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let chatOptions = [];
|
let chatOptions = [];
|
||||||
function createCopyParentText(message) {
|
function createCopyParentText(message) {
|
||||||
|
@ -58,352 +58,6 @@
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatDate(date) {
|
|
||||||
// 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) {
|
|
||||||
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) {
|
|
||||||
|
|
||||||
// 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") {
|
|
||||||
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");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processOnlineReferences(referenceSection, onlineContext) {
|
|
||||||
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) {
|
|
||||||
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) {
|
|
||||||
let imageMarkdown;
|
|
||||||
if (intentType === "text-to-image") {
|
|
||||||
imageMarkdown = `![](data:image/png;base64,${message})`;
|
|
||||||
} else if (intentType === "text-to-image2") {
|
|
||||||
imageMarkdown = `![](${message})`;
|
|
||||||
} else if (intentType === "text-to-image-v3") {
|
|
||||||
imageMarkdown = `![](data:image/webp;base64,${message})`;
|
|
||||||
}
|
|
||||||
const inferredQuery = inferredQueries?.[0];
|
|
||||||
if (inferredQuery) {
|
|
||||||
imageMarkdown += `\n\n**Inferred Query**:\n\n${inferredQuery}`;
|
|
||||||
}
|
|
||||||
return imageMarkdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatHTMLMessage(message, raw=false, willReplace=true) {
|
|
||||||
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.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);
|
|
||||||
|
|
||||||
// Check if hijs has been loaded
|
|
||||||
if (typeof hljs !== 'undefined') {
|
|
||||||
// Highlight the code block
|
|
||||||
hljs.highlightBlock(codeElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a copy button to each element
|
|
||||||
if (willReplace === true) {
|
|
||||||
let copyButton = document.createElement('button');
|
|
||||||
copyButton.classList.add("copy-button");
|
|
||||||
copyButton.title = "Copy Code";
|
|
||||||
let copyIcon = document.createElement("img");
|
|
||||||
copyIcon.src = "./assets/icons/copy-button.svg";
|
|
||||||
copyIcon.classList.add("copy-icon");
|
|
||||||
copyButton.appendChild(copyIcon);
|
|
||||||
copyButton.addEventListener('click', copyParentText);
|
|
||||||
codeElement.prepend(copyButton);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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.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);
|
|
||||||
referencesDiv.appendChild(referenceSection);
|
|
||||||
|
|
||||||
return referencesDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chat() {
|
async function chat() {
|
||||||
// Extract required fields for search from form
|
// Extract required fields for search from form
|
||||||
let query = document.getElementById("chat-input").value.trim();
|
let query = document.getElementById("chat-input").value.trim();
|
||||||
|
|
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 = `![](data:image/png;base64,${message})`;
|
||||||
|
} else if (intentType === "text-to-image2") {
|
||||||
|
imageMarkdown = `![](${message})`;
|
||||||
|
} else if (intentType === "text-to-image-v3") {
|
||||||
|
imageMarkdown = `![](data:image/webp;base64,${message})`;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
|
@ -431,6 +431,9 @@ function addCSPHeaderToSession () {
|
||||||
let firstRun = true;
|
let firstRun = true;
|
||||||
let win = null;
|
let win = null;
|
||||||
let titleBarStyle = process.platform === 'win32' ? 'default' : 'hidden';
|
let titleBarStyle = process.platform === 'win32' ? 'default' : 'hidden';
|
||||||
|
const {globalShortcut, clipboard} = require('electron'); // global shortcut and clipboard dependencies for shortcut window
|
||||||
|
const openShortcutWindowKeyBind = 'CommandOrControl+Shift+K'
|
||||||
|
|
||||||
const createWindow = (tab = 'chat.html') => {
|
const createWindow = (tab = 'chat.html') => {
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
width: 800,
|
width: 800,
|
||||||
|
@ -506,6 +509,48 @@ const createWindow = (tab = 'chat.html') => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createShortcutWindow = (tab = 'shortcut.html') => {
|
||||||
|
var shortcutWin = new BrowserWindow({
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
show: false,
|
||||||
|
titleBarStyle: titleBarStyle,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
frame: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nodeIntegration: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
shortcutWin.setMenuBarVisibility(false);
|
||||||
|
shortcutWin.setResizable(false);
|
||||||
|
shortcutWin.setOpacity(0.95);
|
||||||
|
shortcutWin.setBackgroundColor('#f5f4f3');
|
||||||
|
shortcutWin.setHasShadow(true);
|
||||||
|
shortcutWin.setVibrancy('popover');
|
||||||
|
|
||||||
|
shortcutWin.loadFile(tab);
|
||||||
|
shortcutWin.once('ready-to-show', () => {
|
||||||
|
shortcutWin.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
shortcutWin.on('closed', () => {
|
||||||
|
shortcutWin = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return shortcutWin;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isShortcutWindowOpen() {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
for (let i = 0; i < windows.length; i++) {
|
||||||
|
if (windows[i].webContents.getURL().endsWith('shortcut.html')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
addCSPHeaderToSession();
|
addCSPHeaderToSession();
|
||||||
|
|
||||||
|
@ -551,14 +596,13 @@ app.whenReady().then(() => {
|
||||||
});
|
});
|
||||||
ipcMain.handle('deleteAllFiles', deleteAllFiles);
|
ipcMain.handle('deleteAllFiles', deleteAllFiles);
|
||||||
|
|
||||||
createWindow();
|
const mainWindow = createWindow();
|
||||||
|
|
||||||
|
|
||||||
app.setAboutPanelOptions({
|
app.setAboutPanelOptions({
|
||||||
applicationName: "Khoj",
|
applicationName: "Khoj",
|
||||||
applicationVersion: khojPackage.version,
|
applicationVersion: khojPackage.version,
|
||||||
version: khojPackage.version,
|
version: khojPackage.version,
|
||||||
authors: "Saba Imran, Debanjum Singh Solanky and contributors",
|
authors: "Khoj AI",
|
||||||
website: "https://khoj.dev",
|
website: "https://khoj.dev",
|
||||||
copyright: "GPL v3",
|
copyright: "GPL v3",
|
||||||
iconPath: path.join(__dirname, 'assets', 'icons', 'favicon-128x128.png')
|
iconPath: path.join(__dirname, 'assets', 'icons', 'favicon-128x128.png')
|
||||||
|
@ -575,9 +619,43 @@ app.whenReady().then(() => {
|
||||||
console.warn("Desktop app update check failed:", e);
|
console.warn("Desktop app update check failed:", e);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
globalShortcut.register(openShortcutWindowKeyBind, () => {
|
||||||
|
console.log("Shortcut key pressed")
|
||||||
|
if(isShortcutWindowOpen()) return;
|
||||||
|
|
||||||
|
const shortcutWin = createShortcutWindow(); // Create a new shortcut window each time the shortcut is triggered
|
||||||
|
shortcutWin.setAlwaysOnTop(true, 'screen-saver', 1);
|
||||||
|
const clipboardText = clipboard.readText();
|
||||||
|
console.log('Sending clipboard text:', clipboardText); // Debug log
|
||||||
|
shortcutWin.webContents.once('dom-ready', () => {
|
||||||
|
shortcutWin.webContents.send('clip', clipboardText);
|
||||||
|
console.log('Message sent to window'); // Debug log
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register a global shortcut for the Escape key for the shortcutWin
|
||||||
|
globalShortcut.register('Escape', () => {
|
||||||
|
if (shortcutWin) {
|
||||||
|
shortcutWin.close();
|
||||||
|
}
|
||||||
|
// Unregister the Escape key shortcut
|
||||||
|
globalShortcut.unregister('Escape');
|
||||||
|
});
|
||||||
|
|
||||||
|
shortcutWin.on('closed', () => {
|
||||||
|
// Unregister the Escape key shortcut
|
||||||
|
globalShortcut.unregister('Escape');
|
||||||
|
});
|
||||||
|
ipcMain.on('continue-conversation-button-clicked', () => {
|
||||||
|
openWindow('chat.html');
|
||||||
|
if (shortcutWin && !shortcutWin.isDestroyed()) {
|
||||||
|
shortcutWin.close();
|
||||||
|
}
|
||||||
|
// Unregister the Escape key shortcut
|
||||||
|
globalShortcut.unregister('Escape');
|
||||||
|
});
|
||||||
|
});
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
setTitle: (title) => ipcRenderer.send('set-title', title)
|
setTitle: (title) => ipcRenderer.send('set-title', title)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('clipboardAPI', {
|
||||||
|
sendClipboardText: (callback) => {
|
||||||
|
ipcRenderer.on('clip', (event, message) => {
|
||||||
|
callback(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('routeBackToMainWindowAPI', {
|
||||||
|
sendSignal: () => {
|
||||||
|
ipcRenderer.send('continue-conversation-button-clicked'); // Custom event name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('storeValueAPI', {
|
contextBridge.exposeInMainWorld('storeValueAPI', {
|
||||||
handleFileOpen: (key) => ipcRenderer.invoke('handleFileOpen', key)
|
handleFileOpen: (key) => ipcRenderer.invoke('handleFileOpen', key)
|
||||||
})
|
})
|
||||||
|
|
518
src/interface/desktop/shortcut.html
Normal file
518
src/interface/desktop/shortcut.html
Normal file
|
@ -0,0 +1,518 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Khoj Mini</title>
|
||||||
|
<style>
|
||||||
|
#title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #f9f5de;
|
||||||
|
color: black;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
z-index: 9999;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
#loading-dots {
|
||||||
|
padding-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
#styled-input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 100px;
|
||||||
|
min-height: 50px;
|
||||||
|
background-color: #475569; /* Blue background */
|
||||||
|
color: #dcdfe4; /* White text */
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
border: none;
|
||||||
|
resize: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.chat-input {
|
||||||
|
margin-top: 10px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 5;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 10px;
|
||||||
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
#input-container {
|
||||||
|
background-color: #475569;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
#send-button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px 5px 5px 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
background-color: #5a6b84;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
position: relative;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
#send-button:hover {
|
||||||
|
background: #7489a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
#edit-button {
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px 5px 5px 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
background-color: #5a6b84;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#edit-button:hover {
|
||||||
|
background: #7489a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 5px; /* Width of the scrollbar */
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
/* * {
|
||||||
|
outline: 1px solid rgb(255, 255, 255);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Track */
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1; /* Background of the scrollbar track */
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle */
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #2d2d2d; /* Color of the scrollbar thumb */
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle on hover */
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #000000; /* Color of the scrollbar thumb on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
#copy-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
#reference-expand-button{
|
||||||
|
background-color: #000000;
|
||||||
|
color: #dcdfe4; /* White text */
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#linker-button{
|
||||||
|
background-color: #fee285;
|
||||||
|
color: black; /* White text */
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
#linker-button:hover {
|
||||||
|
background-color: #f9f5de;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.expanded {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* CSS for the container */
|
||||||
|
.logo-container {
|
||||||
|
/* display: flex; Use flexbox to align items */
|
||||||
|
align-items: center; /* Align items vertically */
|
||||||
|
justify-content: flex;
|
||||||
|
padding: 10px; /* Add padding to the container */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CSS for the image */
|
||||||
|
img {
|
||||||
|
width: 100px; /* Set the desired width */
|
||||||
|
height: auto; /* Allows the image to scale proportionally */
|
||||||
|
}
|
||||||
|
.clipboardText {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
background-color: #475569;
|
||||||
|
color: #dcdfe4;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.reference-button {
|
||||||
|
background-color: #dde5f0;
|
||||||
|
color: #dcdfe4;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
#chat-body {
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
b {
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
font-family:'Noto Sans', sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
scrollbar-width: 5px; /* "auto" or "thin" */
|
||||||
|
scrollbar-color: white white;/* thumb color and track color */
|
||||||
|
height: 300px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
#chat-body-wrapper {
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-dots span {
|
||||||
|
display: inline-block;
|
||||||
|
animation: bounce 0.9s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot1 {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot2 {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot3 {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="title-bar">Khoj (Esc to Quit)</div>
|
||||||
|
<div id="chat-body-wrapper">
|
||||||
|
<div id="input-container">
|
||||||
|
<textarea id="styled-input" name="styled-input">Hello World!</textarea>
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
if (!window.clipboardAPI) {
|
||||||
|
throw new Error('clipboardAPI is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clipboardAPI.sendClipboardText((clipboardText) => {
|
||||||
|
try {
|
||||||
|
const styledInput = document.getElementById('styled-input');
|
||||||
|
if (!styledInput) {
|
||||||
|
throw new Error('styled-input element not found');
|
||||||
|
}
|
||||||
|
styledInput.value = clipboardText;
|
||||||
|
console.log("success: ", clipboardText);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling clipboard text:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting up clipboard listener:', error);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div style="display: flex;">
|
||||||
|
<button id="send-button" onclick="chat()">
|
||||||
|
Send
|
||||||
|
<svg style="margin-left: 3px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send">
|
||||||
|
<path d="M5 12l10 0-5-5m5 5-5 5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="edit-button" onclick="edit()">
|
||||||
|
Edit
|
||||||
|
<svg style="margin-left: 6px; margin-right: 6px;" fill="#fff" height="11px" width="11px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="loading-dots" style="display: none;"></div>
|
||||||
|
<div id="chat-body"></div>
|
||||||
|
</div>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
<script type="text/javascript" src="./assets/purify.min.js?v={{ khoj_version }}"></script>
|
||||||
|
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
|
||||||
|
<script src="./utils.js"></script>
|
||||||
|
<script src="./chatutils.js"></script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let region = null;
|
||||||
|
let city = null;
|
||||||
|
let countryName = null;
|
||||||
|
let timezone = null;
|
||||||
|
|
||||||
|
fetch("https://ipapi.co/json")
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
region = data.region;
|
||||||
|
city = data.city;
|
||||||
|
countryName = data.country_name;
|
||||||
|
timezone = data.timezone;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleLoading() {
|
||||||
|
var dots = document.getElementById('loading-dots');
|
||||||
|
if (dots.style.display === 'none') {
|
||||||
|
dots.innerHTML = 'Loading<span class="dot1">.</span><span class="dot2">.</span><span class="dot3">.</span>';
|
||||||
|
dots.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
dots.innerHTML = '';
|
||||||
|
dots.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit() {
|
||||||
|
//enable input for text area
|
||||||
|
let inp = document.getElementById("styled-input");
|
||||||
|
inp.removeAttribute('readonly');
|
||||||
|
//put focus on text area
|
||||||
|
inp.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function chat() {
|
||||||
|
//set chat body to empty
|
||||||
|
let chatBody = document.getElementById("chat-body");
|
||||||
|
chatBody.innerHTML = "";
|
||||||
|
toggleLoading();
|
||||||
|
let inp = document.getElementById("styled-input");
|
||||||
|
query = inp.value;
|
||||||
|
inp.setAttribute('readonly', true);
|
||||||
|
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
||||||
|
console.log(`Query: ${query}`);
|
||||||
|
|
||||||
|
// Short circuit on empty query
|
||||||
|
if (query.length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
let chat_body = document.getElementById("chat-body");
|
||||||
|
|
||||||
|
let conversationID = chat_body.dataset.conversationId;
|
||||||
|
let hostURL = await window.hostURLAPI.getURL();
|
||||||
|
const khojToken = await window.tokenAPI.getToken();
|
||||||
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||||
|
|
||||||
|
if (!conversationID) {
|
||||||
|
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
|
||||||
|
let data = await response.json();
|
||||||
|
conversationID = data.conversation_id;
|
||||||
|
chat_body.dataset.conversationId = conversationID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate backend API URL to execute query
|
||||||
|
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
|
||||||
|
|
||||||
|
let newResponseEl = document.createElement("div");
|
||||||
|
newResponseEl.classList.add("chat-message", "khoj");
|
||||||
|
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
||||||
|
chat_body.appendChild(newResponseEl);
|
||||||
|
|
||||||
|
let newResponseTextEl = document.createElement("div");
|
||||||
|
newResponseTextEl.classList.add("chat-message-text", "khoj");
|
||||||
|
newResponseEl.appendChild(newResponseTextEl);
|
||||||
|
|
||||||
|
// Temporary status message to indicate that Khoj is thinking
|
||||||
|
let loadingEllipsis = document.createElement("div");
|
||||||
|
loadingEllipsis.classList.add("lds-ellipsis");
|
||||||
|
|
||||||
|
let firstEllipsis = document.createElement("div");
|
||||||
|
firstEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let secondEllipsis = document.createElement("div");
|
||||||
|
secondEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let thirdEllipsis = document.createElement("div");
|
||||||
|
thirdEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
let fourthEllipsis = document.createElement("div");
|
||||||
|
fourthEllipsis.classList.add("lds-ellipsis-item");
|
||||||
|
|
||||||
|
loadingEllipsis.appendChild(firstEllipsis);
|
||||||
|
loadingEllipsis.appendChild(secondEllipsis);
|
||||||
|
loadingEllipsis.appendChild(thirdEllipsis);
|
||||||
|
loadingEllipsis.appendChild(fourthEllipsis);
|
||||||
|
|
||||||
|
newResponseTextEl.appendChild(loadingEllipsis);
|
||||||
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
|
||||||
|
// Call Khoj chat API
|
||||||
|
let response = await fetch(chatApi, { headers });
|
||||||
|
let rawResponse = "";
|
||||||
|
let references = null;
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
toggleLoading();
|
||||||
|
if (contentType === "application/json") {
|
||||||
|
// Handle JSON response
|
||||||
|
try {
|
||||||
|
const responseAsJson = await response.json();
|
||||||
|
if (responseAsJson.image) {
|
||||||
|
// If response has image field, response is a generated image.
|
||||||
|
if (responseAsJson.intentType === "text-to-image") {
|
||||||
|
rawResponse += `![${query}](data:image/png;base64,${responseAsJson.image})`;
|
||||||
|
} else if (responseAsJson.intentType === "text-to-image2") {
|
||||||
|
rawResponse += `![${query}](${responseAsJson.image})`;
|
||||||
|
} else if (responseAsJson.intentType === "text-to-image-v3") {
|
||||||
|
rawResponse += `![${query}](data:image/webp;base64,${responseAsJson.image})`;
|
||||||
|
}
|
||||||
|
const inferredQueries = responseAsJson.inferredQueries?.[0];
|
||||||
|
if (inferredQueries) {
|
||||||
|
rawResponse += `\n\n**Inferred Query**:\n\n${inferredQueries}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (responseAsJson.context) {
|
||||||
|
const rawReferenceAsJson = responseAsJson.context;
|
||||||
|
references = createReferenceSection(rawReferenceAsJson, createLinkerSection=true);
|
||||||
|
}
|
||||||
|
if (responseAsJson.detail) {
|
||||||
|
// If response has detail field, response is an error message.
|
||||||
|
rawResponse += responseAsJson.detail;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If the chunk is not a JSON object, just display it as is
|
||||||
|
rawResponse += chunk;
|
||||||
|
} finally {
|
||||||
|
newResponseTextEl.innerHTML = "";
|
||||||
|
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||||
|
|
||||||
|
if (references != null) {
|
||||||
|
newResponseTextEl.appendChild(references);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle streamed response of type text/event-stream or text/plain
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let references = {};
|
||||||
|
|
||||||
|
readStream();
|
||||||
|
|
||||||
|
function readStream() {
|
||||||
|
reader.read().then(({ done, value }) => {
|
||||||
|
if (done) {
|
||||||
|
// Append any references after all the data has been streamed
|
||||||
|
if (references != {}) {
|
||||||
|
newResponseTextEl.appendChild(createReferenceSection(references, createLinkerSection=true));
|
||||||
|
}
|
||||||
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode message chunk from stream
|
||||||
|
const chunk = decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
if (chunk.includes("### compiled references:")) {
|
||||||
|
const additionalResponse = chunk.split("### compiled references:")[0];
|
||||||
|
rawResponse += additionalResponse;
|
||||||
|
newResponseTextEl.innerHTML = "";
|
||||||
|
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||||
|
|
||||||
|
const rawReference = chunk.split("### compiled references:")[1];
|
||||||
|
const rawReferenceAsJson = JSON.parse(rawReference);
|
||||||
|
if (rawReferenceAsJson instanceof Array) {
|
||||||
|
references["notes"] = rawReferenceAsJson;
|
||||||
|
} else if (typeof rawReferenceAsJson === "object" && rawReferenceAsJson !== null) {
|
||||||
|
references["online"] = rawReferenceAsJson;
|
||||||
|
}
|
||||||
|
readStream();
|
||||||
|
} else {
|
||||||
|
// Display response from Khoj
|
||||||
|
if (newResponseTextEl.getElementsByClassName("lds-ellipsis").length > 0) {
|
||||||
|
newResponseTextEl.removeChild(loadingEllipsis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the chunk is not a JSON object, just display it as is
|
||||||
|
rawResponse += chunk;
|
||||||
|
newResponseTextEl.innerHTML = "";
|
||||||
|
newResponseTextEl.appendChild(formatHTMLMessage(rawResponse));
|
||||||
|
|
||||||
|
readStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom of chat window as chat response is streamed
|
||||||
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue