Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OS Level Shortcut Window for Quick Access to Khoj Desktop #815

Merged
merged 23 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8a1cea0
rough sketch of desktop shortcuts. many bugs to fix still
MythicalCow Jun 10, 2024
438c7df
working MVP of desktop shortcut khoj
MythicalCow Jun 10, 2024
b826b93
UI fixes
MythicalCow Jun 10, 2024
53c461b
UI improvements for editable shortcut message
MythicalCow Jun 12, 2024
1bb7d31
major rendering fix to prevent clipboard text from getting lost
MythicalCow Jun 12, 2024
81ec305
UI improvements and bug fixes
MythicalCow Jun 12, 2024
2c2a13d
UI upgrades: custom top bar, edit sent message and color matching
MythicalCow Jun 12, 2024
33d0c07
Merge branch 'khoj-ai:master' into features/desktop-shortcut
MythicalCow Jun 12, 2024
e496d5a
removed debug javascript file
MythicalCow Jun 12, 2024
64fbac1
Merge branch 'features/desktop-shortcut' of https://github.com/Mythic…
MythicalCow Jun 12, 2024
b29b90f
font reverted to Noto Sans
MythicalCow Jun 17, 2024
d09fcc9
resolving diffs
MythicalCow Jun 17, 2024
b59a8ad
resolving diffs
MythicalCow Jun 17, 2024
d105afc
whitespace diff
MythicalCow Jun 17, 2024
09bd4f1
cleaning up the code and removing diffs
MythicalCow Jun 17, 2024
b348323
UX fixes
MythicalCow Jun 17, 2024
66028d3
cleaning up unused methods from html
MythicalCow Jun 17, 2024
7d3e862
front end for button to send user back to main window to continue con…
MythicalCow Jun 17, 2024
b37aeec
UX fix for window and continue conversation support added
MythicalCow Jun 18, 2024
82bcc73
migrated common js functions into chatutils.js
MythicalCow Jun 18, 2024
81ce0f6
Fix window closing issue in macos by
sabaimran Jun 19, 2024
7182043
removed extra comment and renamed continue convo button
MythicalCow Jun 20, 2024
68034b4
Merge branch 'features/desktop-shortcut' of https://github.com/Mythic…
MythicalCow Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 1 addition & 347 deletions src/interface/desktop/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<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="./utils.js"></script>

<script src="chatutils.js"></script>
<script>
let chatOptions = [];
function createCopyParentText(message) {
Expand Down Expand Up @@ -58,352 +58,6 @@
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('"', '&quot;');

// 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() {
// Extract required fields for search from form
let query = document.getElementById("chat-input").value.trim();
Expand Down
Loading
Loading