mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-12-20 11:27:46 +00:00
69344a6aa6
* Add chat sessions to the desktop application * Increase width of the main chat body to 90vw * Update the version of electron * Render the default message if chat history fails to load * Merge conversation migrations and fix slug setting * Update the welcome message, use the hostURL, and update background color for chat actions * Only update the window's web contents if the page is config
1764 lines
75 KiB
HTML
1764 lines
75 KiB
HTML
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
|
|
<title>Khoj - Chat</title>
|
|
|
|
<link rel="icon" type="image/png" sizes="128x128" href="./assets/icons/favicon-128x128.png">
|
|
<link rel="manifest" href="/static/khoj_chat.webmanifest">
|
|
<link rel="stylesheet" href="./assets/khoj.css">
|
|
</head>
|
|
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
|
|
<script src="./utils.js"></script>
|
|
|
|
<script>
|
|
let chatOptions = [];
|
|
function copyProgrammaticOutput(event) {
|
|
// Remove the first 4 characters which are the "Copy" button
|
|
const originalCopyText = event.target.parentNode.textContent.trim().slice(0, 4);
|
|
const programmaticOutput = event.target.parentNode.textContent.trim().slice(4);
|
|
navigator.clipboard.writeText(programmaticOutput).then(() => {
|
|
event.target.textContent = "✅ Copied to clipboard!";
|
|
setTimeout(() => {
|
|
event.target.textContent = originalCopyText;
|
|
}, 1000);
|
|
}).catch((error) => {
|
|
console.error("Error copying programmatic output to clipboard:", error);
|
|
event.target.textContent = "⛔️ Failed to copy!";
|
|
setTimeout(() => {
|
|
event.target.textContent = originalCopyText;
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
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(reference, index) {
|
|
// 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;
|
|
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.innerHTML = 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) {
|
|
let message_time = formatDate(dt ?? new Date());
|
|
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
|
let formattedMessage = formatHTMLMessage(message, raw);
|
|
let chatBody = document.getElementById("chat-body");
|
|
|
|
// Create a new div for the chat message
|
|
let chatMessage = document.createElement('div');
|
|
chatMessage.className = `chat-message ${by}`;
|
|
chatMessage.dataset.meta = `${by_name} at ${message_time}`;
|
|
|
|
// Create a new div for the chat message text and append it to the chat message
|
|
let chatMessageText = document.createElement('div');
|
|
chatMessageText.className = `chat-message-text ${by}`;
|
|
chatMessageText.appendChild(formattedMessage);
|
|
chatMessage.appendChild(chatMessageText);
|
|
|
|
// Append annotations div to the chat message
|
|
if (annotations) {
|
|
chatMessageText.appendChild(annotations);
|
|
}
|
|
|
|
// Append chat message div to chat body
|
|
chatBody.appendChild(chatMessage);
|
|
|
|
// Scroll to bottom of chat-body element
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
return numOnlineReferences;
|
|
}
|
|
|
|
function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) {
|
|
if (intentType === "text-to-image") {
|
|
let imageMarkdown = `![](data:image/png;base64,${message})`;
|
|
imageMarkdown += "\n\n";
|
|
if (inferredQueries) {
|
|
const inferredQuery = inferredQueries?.[0];
|
|
imageMarkdown += `**Inferred Query**: ${inferredQuery}`;
|
|
}
|
|
renderMessage(imageMarkdown, by, dt);
|
|
return;
|
|
}
|
|
|
|
if (context == null && onlineContext == null) {
|
|
renderMessage(message, by, dt);
|
|
return;
|
|
}
|
|
|
|
if ((context && context.length == 0) && (onlineContext == null || (onlineContext && Object.keys(onlineContext).length == 0))) {
|
|
renderMessage(message, by, dt);
|
|
return;
|
|
}
|
|
|
|
let references = document.createElement('div');
|
|
|
|
let referenceExpandButton = document.createElement('button');
|
|
referenceExpandButton.classList.add("reference-expand-button");
|
|
let numReferences = 0;
|
|
|
|
if (context) {
|
|
numReferences += context.length;
|
|
}
|
|
|
|
references.appendChild(referenceExpandButton);
|
|
|
|
let referenceSection = document.createElement('div');
|
|
referenceSection.classList.add("reference-section");
|
|
referenceSection.classList.add("collapsed");
|
|
|
|
referenceExpandButton.addEventListener('click', function() {
|
|
if (referenceSection.classList.contains("collapsed")) {
|
|
referenceSection.classList.remove("collapsed");
|
|
referenceSection.classList.add("expanded");
|
|
} else {
|
|
referenceSection.classList.add("collapsed");
|
|
referenceSection.classList.remove("expanded");
|
|
}
|
|
});
|
|
|
|
references.classList.add("references");
|
|
if (context) {
|
|
for (let index in context) {
|
|
let reference = context[index];
|
|
let polishedReference = generateReference(reference, index);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
|
|
if (onlineContext) {
|
|
numReferences += processOnlineReferences(referenceSection, onlineContext);
|
|
}
|
|
|
|
let expandButtonText = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
|
referenceExpandButton.innerHTML = expandButtonText;
|
|
|
|
references.appendChild(referenceSection);
|
|
|
|
renderMessage(message, by, dt, references);
|
|
}
|
|
|
|
function formatHTMLMessage(htmlMessage, raw=false) {
|
|
var md = window.markdownit();
|
|
let newHTML = htmlMessage;
|
|
|
|
// 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);
|
|
// Get any elements with a class that starts with "language"
|
|
let element = document.createElement('div');
|
|
element.innerHTML = newHTML;
|
|
let codeBlockElements = element.querySelectorAll('[class^="language-"]');
|
|
// For each element, add a parent div with the class "programmatic-output"
|
|
codeBlockElements.forEach((codeElement) => {
|
|
// 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
|
|
let copyButton = document.createElement('button');
|
|
copyButton.classList.add("copy-button");
|
|
copyButton.innerHTML = "Copy";
|
|
copyButton.addEventListener('click', copyProgrammaticOutput);
|
|
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) => {
|
|
// Add the class "inline-chat-link" to each element
|
|
anchorElement.classList.add("inline-chat-link");
|
|
});
|
|
|
|
return element
|
|
}
|
|
|
|
async function chat() {
|
|
// Extract required fields for search from form
|
|
let query = document.getElementById("chat-input").value.trim();
|
|
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
|
console.log(`Query: ${query}`);
|
|
|
|
// Short circuit on empty query
|
|
if (query.length === 0)
|
|
return;
|
|
|
|
// Add message by user to chat body
|
|
renderMessage(query, "you");
|
|
document.getElementById("chat-input").value = "";
|
|
autoResize();
|
|
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
|
|
|
let chat_body = document.getElementById("chat-body");
|
|
|
|
let conversationID = chat_body.dataset.conversationId;
|
|
|
|
let hostURL = await window.hostURLAPI.getURL();
|
|
|
|
if (!conversationID) {
|
|
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST" });
|
|
let data = await response.json();
|
|
conversationID = data.conversation_id;
|
|
chat_body.dataset.conversationId = conversationID;
|
|
}
|
|
|
|
|
|
// Generate backend API URL to execute query
|
|
let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}`;
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
let new_response = document.createElement("div");
|
|
new_response.classList.add("chat-message", "khoj");
|
|
new_response.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
|
chat_body.appendChild(new_response);
|
|
|
|
let newResponseText = document.createElement("div");
|
|
newResponseText.classList.add("chat-message-text", "khoj");
|
|
new_response.appendChild(newResponseText);
|
|
|
|
// Temporary status message to indicate that Khoj is thinking
|
|
let loadingSpinner = document.createElement("div");
|
|
loadingSpinner.classList.add("spinner");
|
|
newResponseText.appendChild(loadingSpinner);
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
|
|
let chatTooltip = document.getElementById("chat-tooltip");
|
|
chatTooltip.style.display = "none";
|
|
|
|
let chatInput = document.getElementById("chat-input");
|
|
chatInput.classList.remove("option-enabled");
|
|
|
|
// Call specified Khoj API
|
|
let response = await fetch(url, { headers });
|
|
let rawResponse = "";
|
|
const contentType = response.headers.get("content-type");
|
|
|
|
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.
|
|
rawResponse += `![${query}](data:image/png;base64,${responseAsJson.image})`;
|
|
rawResponse += "\n\n";
|
|
const inferredQueries = responseAsJson.inferredQueries?.[0];
|
|
if (inferredQueries) {
|
|
rawResponse += `**Inferred Query**: ${inferredQueries}`;
|
|
}
|
|
}
|
|
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 {
|
|
newResponseText.innerHTML = "";
|
|
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
|
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
document.getElementById("chat-input").removeAttribute("disabled");
|
|
}
|
|
} else {
|
|
// Handle streamed response of type text/event-stream or text/plain
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let references = null;
|
|
|
|
readStream();
|
|
|
|
function readStream() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
// Append any references after all the data has been streamed
|
|
if (references != null) {
|
|
newResponseText.appendChild(references);
|
|
}
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
document.getElementById("chat-input").removeAttribute("disabled");
|
|
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;
|
|
newResponseText.innerHTML = "";
|
|
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
|
|
|
const rawReference = chunk.split("### compiled references:")[1];
|
|
const rawReferenceAsJson = JSON.parse(rawReference);
|
|
references = document.createElement('div');
|
|
references.classList.add("references");
|
|
|
|
let referenceExpandButton = document.createElement('button');
|
|
referenceExpandButton.classList.add("reference-expand-button");
|
|
|
|
let referenceSection = document.createElement('div');
|
|
referenceSection.classList.add("reference-section");
|
|
referenceSection.classList.add("collapsed");
|
|
|
|
let numReferences = 0;
|
|
|
|
// If rawReferenceAsJson is a list, then count the length
|
|
if (Array.isArray(rawReferenceAsJson)) {
|
|
numReferences = rawReferenceAsJson.length;
|
|
|
|
rawReferenceAsJson.forEach((reference, index) => {
|
|
let polishedReference = generateReference(reference, index);
|
|
referenceSection.appendChild(polishedReference);
|
|
});
|
|
} else {
|
|
numReferences += processOnlineReferences(referenceSection, rawReferenceAsJson);
|
|
}
|
|
|
|
references.appendChild(referenceExpandButton);
|
|
|
|
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 expandButtonText = numReferences == 1 ? "1 reference" : `${numReferences} references`;
|
|
referenceExpandButton.innerHTML = expandButtonText;
|
|
references.appendChild(referenceSection);
|
|
readStream();
|
|
} else {
|
|
// Display response from Khoj
|
|
if (newResponseText.getElementsByClassName("spinner").length > 0) {
|
|
newResponseText.removeChild(loadingSpinner);
|
|
}
|
|
|
|
// Try to parse the chunk as a JSON object. It will be a JSON object if there is an error.
|
|
if (chunk.startsWith("{") && chunk.endsWith("}")) {
|
|
try {
|
|
const responseAsJson = JSON.parse(chunk);
|
|
if (responseAsJson.image) {
|
|
// If response has image field, response is a generated image.
|
|
rawResponse += `![${query}](data:image/png;base64,${responseAsJson.image})`;
|
|
rawResponse += "\n\n";
|
|
const inferredQueries = responseAsJson.inferredQueries?.[0];
|
|
if (inferredQueries) {
|
|
rawResponse += `**Inferred Query**: ${inferredQueries}`;
|
|
}
|
|
}
|
|
if (responseAsJson.detail) {
|
|
rawResponse += responseAsJson.detail;
|
|
}
|
|
} catch (error) {
|
|
// If the chunk is not a JSON object, just display it as is
|
|
rawResponse += chunk;
|
|
} finally {
|
|
newResponseText.innerHTML = "";
|
|
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
|
}
|
|
} else {
|
|
// If the chunk is not a JSON object, just display it as is
|
|
rawResponse += chunk;
|
|
newResponseText.innerHTML = "";
|
|
newResponseText.appendChild(formatHTMLMessage(rawResponse));
|
|
|
|
readStream();
|
|
}
|
|
}
|
|
|
|
// Scroll to bottom of chat window as chat response is streamed
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function incrementalChat(event) {
|
|
if (!event.shiftKey && event.key === 'Enter') {
|
|
event.preventDefault();
|
|
chat();
|
|
}
|
|
}
|
|
|
|
function onChatInput() {
|
|
let chatInput = document.getElementById("chat-input");
|
|
chatInput.value = chatInput.value.trimStart();
|
|
|
|
let questionStarterSuggestions = document.getElementById("question-starters");
|
|
questionStarterSuggestions.style.display = "none";
|
|
|
|
if (chatInput.value.startsWith("/") && chatInput.value.split(" ").length === 1) {
|
|
let chatTooltip = document.getElementById("chat-tooltip");
|
|
chatTooltip.style.display = "block";
|
|
let helpText = "<div>";
|
|
const command = chatInput.value.split(" ")[0].substring(1);
|
|
for (let key in chatOptions) {
|
|
if (!!!command || key.startsWith(command)) {
|
|
helpText += "<b>/" + key + "</b>: " + chatOptions[key] + "<br>";
|
|
}
|
|
}
|
|
chatTooltip.innerHTML = helpText;
|
|
} else if (chatInput.value.startsWith("/")) {
|
|
const firstWord = chatInput.value.split(" ")[0];
|
|
if (firstWord.substring(1) in chatOptions) {
|
|
chatInput.classList.add("option-enabled");
|
|
} else {
|
|
chatInput.classList.remove("option-enabled");
|
|
}
|
|
let chatTooltip = document.getElementById("chat-tooltip");
|
|
chatTooltip.style.display = "none";
|
|
} else {
|
|
let chatTooltip = document.getElementById("chat-tooltip");
|
|
chatTooltip.style.display = "none";
|
|
chatInput.classList.remove("option-enabled");
|
|
}
|
|
|
|
autoResize();
|
|
}
|
|
|
|
function autoResize() {
|
|
const textarea = document.getElementById('chat-input');
|
|
const scrollTop = textarea.scrollTop;
|
|
textarea.style.height = '0';
|
|
const scrollHeight = textarea.scrollHeight + 8; // +8 accounts for padding
|
|
textarea.style.height = Math.min(scrollHeight, 200) + 'px';
|
|
textarea.scrollTop = scrollTop;
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
}
|
|
|
|
window.addEventListener('load', async() => {
|
|
await loadChat();
|
|
});
|
|
|
|
async function loadChat() {
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
let chatBody = document.getElementById("chat-body");
|
|
let conversationId = chatBody.dataset.conversationId;
|
|
let chatHistoryUrl = `/api/chat/history?client=desktop`;
|
|
if (conversationId) {
|
|
chatHistoryUrl += `&conversation_id=${conversationId}`;
|
|
}
|
|
|
|
fetch(`${hostURL}${chatHistoryUrl}`, { headers })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.detail) {
|
|
// If the server returns a 500 error with detail, render a setup hint.
|
|
first_run_message = `Hi 👋🏾, to get started:
|
|
<ol>
|
|
<li>Generate an API token in the <a class='inline-chat-link' href="#" onclick="window.navigateAPI.navigateToWebSettings()">Khoj Web settings</a></li>
|
|
<li>Paste it into the API Key field in the <a class='inline-chat-link' href="#" onclick="window.navigateAPI.navigateToSettings()">Khoj Desktop settings</a></li>
|
|
</ol>`
|
|
.trim()
|
|
.replace(/(\r\n|\n|\r)/gm, "");
|
|
|
|
renderMessage(first_run_message, "khoj", null, null, true);
|
|
|
|
// Disable chat input field and update placeholder text
|
|
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
|
document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat");
|
|
} else {
|
|
// Set welcome message on load
|
|
renderMessage("Hey 👋🏾, what's up?", "khoj");
|
|
}
|
|
return data.response;
|
|
})
|
|
.then(response => {
|
|
conversationId = response.conversation_id;
|
|
const conversationTitle = response.slug || `New conversation 🌱`;
|
|
|
|
let chatBody = document.getElementById("chat-body");
|
|
chatBody.dataset.conversationId = conversationId;
|
|
chatBody.dataset.conversationTitle = conversationTitle;
|
|
|
|
const fullChatLog = response.chat || [];
|
|
|
|
fullChatLog.forEach(chat_log => {
|
|
if (chat_log.message != null) {
|
|
renderMessageWithReference(
|
|
chat_log.message,
|
|
chat_log.by,
|
|
chat_log.context,
|
|
new Date(chat_log.created),
|
|
chat_log.onlineContext,
|
|
chat_log.intent?.type,
|
|
chat_log.intent?.["inferred-queries"]);
|
|
}
|
|
})
|
|
let chatBodyWrapper = document.getElementById("chat-body-wrapper");
|
|
chatBodyWrapperHeight = chatBodyWrapper.clientHeight;
|
|
|
|
chatBody.style.height = chatBodyWrapperHeight;
|
|
})
|
|
.catch(err => {
|
|
// If the server returns a 500 error with detail, render a setup hint.
|
|
first_run_message = `Hi 👋🏾, to get started:
|
|
<ol>
|
|
<li>Generate an API token in the <a class='inline-chat-link' href="#" onclick="window.navigateAPI.navigateToWebSettings()">Khoj Web settings</a></li>
|
|
<li>Paste it into the API Key field in the <a class='inline-chat-link' href="#" onclick="window.navigateAPI.navigateToSettings()">Khoj Desktop settings</a></li>
|
|
</ol>`
|
|
.trim()
|
|
.replace(/(\r\n|\n|\r)/gm, "");
|
|
|
|
renderMessage(first_run_message, "khoj", null, null, true);
|
|
|
|
// Disable chat input field and update placeholder text
|
|
document.getElementById("chat-input").setAttribute("disabled", "disabled");
|
|
document.getElementById("chat-input").setAttribute("placeholder", "Configure Khoj to enable chat");
|
|
return;
|
|
});
|
|
|
|
|
|
fetch(`${hostURL}/api/chat/sessions`, { method: "GET", headers })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
let conversationListBody = document.getElementById("conversation-list-body");
|
|
conversationListBody.innerHTML = "";
|
|
let conversationListBodyHeader = document.getElementById("conversation-list-header");
|
|
|
|
let chatBody = document.getElementById("chat-body");
|
|
conversationId = chatBody.dataset.conversationId;
|
|
|
|
if (data.length > 0) {
|
|
conversationListBodyHeader.style.display = "block";
|
|
for (let index in data) {
|
|
let conversation = data[index];
|
|
let conversationButton = document.createElement('div');
|
|
let incomingConversationId = conversation["conversation_id"];
|
|
const conversationTitle = conversation["slug"] || `New conversation 🌱`;
|
|
conversationButton.innerHTML = conversationTitle;
|
|
conversationButton.classList.add("conversation-button");
|
|
if (incomingConversationId == conversationId) {
|
|
conversationButton.classList.add("selected-conversation");
|
|
}
|
|
conversationButton.addEventListener('click', function() {
|
|
let chatBody = document.getElementById("chat-body");
|
|
chatBody.innerHTML = "";
|
|
chatBody.dataset.conversationId = incomingConversationId;
|
|
chatBody.dataset.conversationTitle = conversationTitle;
|
|
loadChat();
|
|
});
|
|
let threeDotMenu = document.createElement('div');
|
|
threeDotMenu.classList.add("three-dot-menu");
|
|
let threeDotMenuButton = document.createElement('button');
|
|
threeDotMenuButton.innerHTML = "⋮";
|
|
threeDotMenuButton.classList.add("three-dot-menu-button");
|
|
threeDotMenuButton.addEventListener('click', function(event) {
|
|
event.stopPropagation();
|
|
|
|
let existingChildren = threeDotMenu.children;
|
|
|
|
if (existingChildren.length > 1) {
|
|
// Skip deleting the first, since that's the menu button.
|
|
for (let i = 1; i < existingChildren.length; i++) {
|
|
existingChildren[i].remove();
|
|
}
|
|
return;
|
|
}
|
|
|
|
let conversationMenu = document.createElement('div');
|
|
conversationMenu.classList.add("conversation-menu");
|
|
|
|
let deleteButton = document.createElement('button');
|
|
deleteButton.innerHTML = "Delete";
|
|
deleteButton.classList.add("delete-conversation-button");
|
|
deleteButton.classList.add("three-dot-menu-button-item");
|
|
deleteButton.addEventListener('click', function() {
|
|
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
|
fetch(`${hostURL}${deleteURL}` , { method: "DELETE", headers })
|
|
.then(response => response.ok ? response.json() : Promise.reject(response))
|
|
.then(data => {
|
|
let chatBody = document.getElementById("chat-body");
|
|
chatBody.innerHTML = "";
|
|
chatBody.dataset.conversationId = "";
|
|
chatBody.dataset.conversationTitle = "";
|
|
loadChat();
|
|
})
|
|
.catch(err => {
|
|
return;
|
|
});
|
|
});
|
|
conversationMenu.appendChild(deleteButton);
|
|
threeDotMenu.appendChild(conversationMenu);
|
|
|
|
let editTitleButton = document.createElement('button');
|
|
editTitleButton.innerHTML = "Rename";
|
|
editTitleButton.classList.add("edit-title-button");
|
|
editTitleButton.classList.add("three-dot-menu-button-item");
|
|
editTitleButton.addEventListener('click', function(event) {
|
|
event.stopPropagation();
|
|
|
|
let conversationMenuChildren = conversationMenu.children;
|
|
|
|
let totalItems = conversationMenuChildren.length;
|
|
|
|
for (let i = totalItems - 1; i >= 0; i--) {
|
|
conversationMenuChildren[i].remove();
|
|
}
|
|
|
|
// Create a dialog box to get new title for conversation
|
|
let conversationTitleInputBox = document.createElement('div');
|
|
conversationTitleInputBox.classList.add("conversation-title-input-box");
|
|
let conversationTitleInput = document.createElement('input');
|
|
conversationTitleInput.classList.add("conversation-title-input");
|
|
|
|
conversationTitleInput.value = conversationTitle;
|
|
|
|
conversationTitleInput.addEventListener('click', function(event) {
|
|
event.stopPropagation();
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
conversationTitleInputButton.click();
|
|
}
|
|
});
|
|
|
|
conversationTitleInputBox.appendChild(conversationTitleInput);
|
|
let conversationTitleInputButton = document.createElement('button');
|
|
conversationTitleInputButton.innerHTML = "Save";
|
|
conversationTitleInputButton.classList.add("three-dot-menu-button-item");
|
|
conversationTitleInputButton.addEventListener('click', function(event) {
|
|
event.stopPropagation();
|
|
let newTitle = conversationTitleInput.value;
|
|
if (newTitle != null) {
|
|
let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`;
|
|
fetch(`${hostURL}${editURL}` , { method: "PATCH" })
|
|
.then(response => response.ok ? response.json() : Promise.reject(response))
|
|
.then(data => {
|
|
conversationButton.innerHTML = newTitle;
|
|
})
|
|
.catch(err => {
|
|
return;
|
|
});
|
|
conversationTitleInputBox.remove();
|
|
}});
|
|
conversationTitleInputBox.appendChild(conversationTitleInputButton);
|
|
conversationMenu.appendChild(conversationTitleInputBox);
|
|
});
|
|
conversationMenu.appendChild(editTitleButton);
|
|
threeDotMenu.appendChild(conversationMenu);
|
|
});
|
|
threeDotMenu.appendChild(threeDotMenuButton);
|
|
conversationButton.appendChild(threeDotMenu);
|
|
conversationListBody.appendChild(conversationButton);
|
|
}
|
|
}
|
|
})
|
|
|
|
fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Render chat options, if any
|
|
if (data.length > 0) {
|
|
let questionStarterSuggestions = document.getElementById("question-starters");
|
|
for (let index in data) {
|
|
let questionStarter = data[index];
|
|
let questionStarterButton = document.createElement('button');
|
|
questionStarterButton.innerHTML = questionStarter;
|
|
questionStarterButton.classList.add("question-starter");
|
|
questionStarterButton.addEventListener('click', function() {
|
|
questionStarterSuggestions.style.display = "none";
|
|
document.getElementById("chat-input").value = questionStarter;
|
|
chat();
|
|
});
|
|
questionStarterSuggestions.appendChild(questionStarterButton);
|
|
}
|
|
questionStarterSuggestions.style.display = "grid";
|
|
}
|
|
})
|
|
.catch(err => {
|
|
return;
|
|
});
|
|
|
|
fetch(`${hostURL}/api/chat/options`, { headers })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Render chat options, if any
|
|
if (data) {
|
|
chatOptions = data;
|
|
}
|
|
})
|
|
.catch(err => {
|
|
return;
|
|
});
|
|
|
|
// Fill query field with value passed in URL query parameters, if any.
|
|
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
|
if (query_via_url) {
|
|
document.getElementById("chat-input").value = query_via_url;
|
|
chat();
|
|
}
|
|
}
|
|
|
|
function flashStatusInChatInput(message) {
|
|
// Get chat input element and original placeholder
|
|
let chatInput = document.getElementById("chat-input");
|
|
let originalPlaceholder = chatInput.placeholder;
|
|
// Set placeholder to message
|
|
chatInput.placeholder = message;
|
|
// Reset placeholder after 2 seconds
|
|
setTimeout(() => {
|
|
chatInput.placeholder = originalPlaceholder;
|
|
}, 2000);
|
|
}
|
|
|
|
function createNewConversation() {
|
|
let chatBody = document.getElementById("chat-body");
|
|
chatBody.innerHTML = "";
|
|
flashStatusInChatInput("📝 New conversation started");
|
|
chatBody.dataset.conversationId = "";
|
|
chatBody.dataset.conversationTitle = "";
|
|
renderMessage("Hey 👋🏾, what's up?", "khoj");
|
|
}
|
|
|
|
async function clearConversationHistory() {
|
|
let chatInput = document.getElementById("chat-input");
|
|
let originalPlaceholder = chatInput.placeholder;
|
|
let chatBody = document.getElementById("chat-body");
|
|
let conversationId = chatBody.dataset.conversationId;
|
|
|
|
let deleteURL = `/api/chat/history?client=desktop`;
|
|
if (conversationId) {
|
|
deleteURL += `&conversation_id=${conversationId}`;
|
|
}
|
|
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
fetch(`${hostURL}${deleteURL}`, { method: "DELETE", headers })
|
|
.then(response => response.ok ? response.json() : Promise.reject(response))
|
|
.then(data => {
|
|
chatBody.innerHTML = "";
|
|
chatBody.dataset.conversationId = "";
|
|
chatBody.dataset.conversationTitle = "";
|
|
loadChat();
|
|
flashStatusInChatInput("🗑 Cleared conversation history");
|
|
})
|
|
.catch(err => {
|
|
flashStatusInChatInput("⛔️ Failed to clear conversation history");
|
|
})
|
|
}
|
|
|
|
let sendMessageTimeout;
|
|
let mediaRecorder;
|
|
async function speechToText(event) {
|
|
event.preventDefault();
|
|
const speakButtonImg = document.getElementById('speak-button-img');
|
|
const stopRecordButtonImg = document.getElementById('stop-record-button-img');
|
|
const sendButtonImg = document.getElementById('send-button-img');
|
|
const stopSendButtonImg = document.getElementById('stop-send-button-img');
|
|
const chatInput = document.getElementById('chat-input');
|
|
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
let url = `${hostURL}/api/transcribe?client=desktop`;
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
const sendToServer = (audioBlob) => {
|
|
const formData = new FormData();
|
|
formData.append('file', audioBlob);
|
|
|
|
fetch(url, { method: 'POST', body: formData, headers})
|
|
.then(response => response.ok ? response.json() : Promise.reject(response))
|
|
.then(data => { chatInput.value += data.text.trimStart(); autoResize(); })
|
|
.then(() => {
|
|
// Don't auto-send empty messages
|
|
if (chatInput.value.length === 0) return;
|
|
|
|
// Send message after 3 seconds, unless stop send button is clicked
|
|
sendButtonImg.style.display = 'none';
|
|
stopSendButtonImg.style.display = 'initial';
|
|
|
|
// Start the countdown timer UI
|
|
document.getElementById('countdown-circle').style.animation = "countdown 3s linear 1 forwards";
|
|
|
|
sendMessageTimeout = setTimeout(() => {
|
|
// Revert to showing send-button and hide the stop-send-button
|
|
sendButtonImg.style.display = 'initial';
|
|
stopSendButtonImg.style.display = 'none';
|
|
|
|
// Stop the countdown timer UI
|
|
document.getElementById('countdown-circle').style.animation = "none";
|
|
|
|
// Send message
|
|
chat();
|
|
}, 3000);
|
|
})
|
|
.catch(err => {
|
|
if (err.status === 501) {
|
|
flashStatusInChatInput("⛔️ Configure speech-to-text model on server.")
|
|
} else if (err.status === 422) {
|
|
flashStatusInChatInput("⛔️ Audio file to large to process.")
|
|
} else {
|
|
flashStatusInChatInput("⛔️ Failed to transcribe audio.")
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleRecording = (stream) => {
|
|
const audioChunks = [];
|
|
const recordingConfig = { mimeType: 'audio/webm' };
|
|
mediaRecorder = new MediaRecorder(stream, recordingConfig);
|
|
|
|
mediaRecorder.addEventListener("dataavailable", function(event) {
|
|
if (event.data.size > 0) audioChunks.push(event.data);
|
|
});
|
|
|
|
mediaRecorder.addEventListener("stop", function() {
|
|
const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
|
sendToServer(audioBlob);
|
|
});
|
|
|
|
mediaRecorder.start();
|
|
speakButtonImg.style.display = 'none';
|
|
stopRecordButtonImg.style.display = 'initial';
|
|
};
|
|
|
|
// Toggle recording
|
|
if (!mediaRecorder || mediaRecorder.state === 'inactive' || event.type === 'touchstart') {
|
|
navigator.mediaDevices
|
|
?.getUserMedia({ audio: true })
|
|
.then(handleRecording)
|
|
.catch((e) => {
|
|
flashStatusInChatInput("⛔️ Failed to access microphone");
|
|
});
|
|
} else if (mediaRecorder.state === 'recording' || event.type === 'touchend' || event.type === 'touchcancel') {
|
|
mediaRecorder.stop();
|
|
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
|
mediaRecorder = null;
|
|
speakButtonImg.style.display = 'initial';
|
|
stopRecordButtonImg.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function cancelSendMessage() {
|
|
// Cancel the chat() call if the stop-send-button is clicked
|
|
clearTimeout(sendMessageTimeout);
|
|
|
|
// Revert to showing send-button and hide the stop-send-button
|
|
document.getElementById('stop-send-button-img').style.display = 'none';
|
|
document.getElementById('send-button-img').style.display = 'initial';
|
|
|
|
// Stop the countdown timer UI
|
|
document.getElementById('countdown-circle').style.animation = "none";
|
|
};
|
|
|
|
function handleCollapseSidePanel() {
|
|
document.getElementById('side-panel').classList.toggle('collapsed');
|
|
document.getElementById('new-conversation').classList.toggle('collapsed');
|
|
document.getElementById('existing-conversations').classList.toggle('collapsed');
|
|
|
|
document.getElementById('chat-section-wrapper').classList.toggle('mobile-friendly');
|
|
}
|
|
</script>
|
|
<body>
|
|
<div id="khoj-empty-container" class="khoj-empty-container">
|
|
</div>
|
|
|
|
<!--Add Header Logo and Nav Pane-->
|
|
<div class="khoj-header">
|
|
<a class="khoj-logo" href="/">
|
|
<img class="khoj-logo" src="./assets/icons/khoj-logo-sideways-500.png" alt="Khoj"></img>
|
|
</a>
|
|
<nav class="khoj-nav">
|
|
<a class="khoj-nav khoj-nav-selected" href="./chat.html">💬 Chat</a>
|
|
<a class="khoj-nav" href="./search.html">🔎 Search</a>
|
|
<a class="khoj-nav" href="./config.html">⚙️ Settings</a>
|
|
</nav>
|
|
</div>
|
|
|
|
<div id="chat-section-wrapper">
|
|
<div id="side-panel-wrapper">
|
|
<div id="side-panel">
|
|
<div id="new-conversation">
|
|
<button class="side-panel-button" id="new-conversation-button" onclick="createNewConversation()">
|
|
New Topic
|
|
<svg class="new-convo-button" viewBox="0 0 35 35" fill="#000000" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M16 0c-8.836 0-16 7.163-16 16s7.163 16 16 16c8.837 0 16-7.163 16-16s-7.163-16-16-16zM16 30.032c-7.72 0-14-6.312-14-14.032s6.28-14 14-14 14 6.28 14 14-6.28 14.032-14 14.032zM23 15h-6v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v6h-6c-0.552 0-1 0.448-1 1s0.448 1 1 1h6v6c0 0.552 0.448 1 1 1s1-0.448 1-1v-6h6c0.552 0 1-0.448 1-1s-0.448-1-1-1z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="existing-conversations">
|
|
<div id="conversation-list">
|
|
<div id="conversation-list-header" style="display: none;">Recent Conversations</div>
|
|
<div id="conversation-list-body"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="collapse-side-panel">
|
|
<button
|
|
class="side-panel-button"
|
|
id="collapse-side-panel-button"
|
|
onclick="handleCollapseSidePanel()"
|
|
>
|
|
<svg class="side-panel-collapse" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M7.82054 20.7313C8.21107 21.1218 8.84423 21.1218 9.23476 20.7313L15.8792 14.0868C17.0505 12.9155 17.0508 11.0167 15.88 9.84497L9.3097 3.26958C8.91918 2.87905 8.28601 2.87905 7.89549 3.26958C7.50497 3.6601 7.50497 4.29327 7.89549 4.68379L14.4675 11.2558C14.8581 11.6464 14.8581 12.2795 14.4675 12.67L7.82054 19.317C7.43002 19.7076 7.43002 20.3407 7.82054 20.7313Z" fill="#0F0F0F"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="chat-body-wrapper">
|
|
<!-- Chat Body -->
|
|
<div id="chat-body"></div>
|
|
|
|
<!-- Chat Suggestions -->
|
|
<div id="question-starters" style="display: none;"></div>
|
|
|
|
<!-- Chat Footer -->
|
|
<div id="chat-footer">
|
|
<div id="chat-tooltip" style="display: none;"></div>
|
|
<div id="input-row">
|
|
<button id="clear-chat-button" class="input-row-button" onclick="clearConversationHistory()">
|
|
<svg class="input-row-button-img" alt="Clear Chat History" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
|
<rect width="128" height="128" fill="none"/>
|
|
<line x1="216" y1="56" x2="40" y2="56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
|
<line x1="104" y1="104" x2="104" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
|
<line x1="152" y1="104" x2="152" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
|
<path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
|
<path d="M168,56V40a16,16,0,0,0-16-16H104A16,16,0,0,0,88,40V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
|
</svg>
|
|
</button>
|
|
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands"></textarea>
|
|
<button id="speak-button" class="input-row-button"
|
|
ontouchstart="speechToText(event)" ontouchend="speechToText(event)" ontouchcancel="speechToText(event)" onmousedown="speechToText(event)">
|
|
<svg id="speak-button-img" class="input-row-button-img" alt="Transcribe" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
|
|
<path d="M10 8a2 2 0 1 1-4 0V3a2 2 0 1 1 4 0v5zM8 0a3 3 0 0 0-3 3v5a3 3 0 0 0 6 0V3a3 3 0 0 0-3-3z"/>
|
|
</svg>
|
|
<svg id="stop-record-button-img" style="display: none" class="input-row-button-img" alt="Stop Transcribing" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
|
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
|
</svg>
|
|
</button>
|
|
<button id="send-button" class="input-row-button" alt="Send message">
|
|
<svg id="send-button-img" onclick="chat()" class="input-row-button-img" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8zm15 0A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-7.5 3.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708l3-3a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V11.5z"/>
|
|
</svg>
|
|
<svg id="stop-send-button-img" onclick="cancelSendMessage()" style="display: none" class="input-row-button-img" alt="Stop Message Send" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
|
<circle id="countdown-circle" class="countdown-circle" cx="8" cy="8" r="7" />
|
|
<path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
<style>
|
|
html, body {
|
|
height: 100%;
|
|
width: 100%;
|
|
padding: 0px;
|
|
margin: 0px;
|
|
}
|
|
body {
|
|
display: grid;
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
text-align: center;
|
|
font-family: var(--font-family);
|
|
font-size: small;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
}
|
|
|
|
body > * {
|
|
padding: 10px;
|
|
margin: 10px;
|
|
}
|
|
|
|
input.conversation-title-input {
|
|
font-family: var(--font-family);
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
padding: 5px;
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 5px;
|
|
margin: 4px;
|
|
}
|
|
|
|
input.conversation-title-input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
#chat-section-wrapper {
|
|
display: grid;
|
|
grid-template-columns: auto auto;
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
padding: 10px;
|
|
margin: 10px;
|
|
overflow-y: scroll;
|
|
}
|
|
|
|
#chat-section-wrapper.mobile-friendly {
|
|
grid-template-columns: auto auto;
|
|
}
|
|
|
|
#chat-body-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#side-panel {
|
|
padding: 10px;
|
|
background: var(--background-color);
|
|
border-radius: 5px;
|
|
box-shadow: 0 0 11px #aaa;
|
|
overflow-y: scroll;
|
|
text-align: left;
|
|
transition: width 0.3s ease-in-out;
|
|
width: 250px;
|
|
}
|
|
|
|
div#side-panel.collapsed {
|
|
width: 1px;
|
|
display: block;
|
|
overflow: hidden;
|
|
}
|
|
|
|
div#collapse-side-panel {
|
|
align-self: center;
|
|
padding: 8px;
|
|
}
|
|
|
|
div#conversation-list-body {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
grid-gap: 8px;
|
|
}
|
|
|
|
div#side-panel-wrapper {
|
|
display: flex
|
|
}
|
|
|
|
|
|
#chat-body {
|
|
font-size: small;
|
|
margin: 0px;
|
|
line-height: 20px;
|
|
overflow-y: scroll;
|
|
overflow-x: hidden;
|
|
}
|
|
/* add chat metatdata to bottom of bubble */
|
|
.chat-message::after {
|
|
content: attr(data-meta);
|
|
display: block;
|
|
font-size: x-small;
|
|
color: #475569;
|
|
margin: -8px 4px 0px 0px;
|
|
}
|
|
/* move message by khoj to left */
|
|
.chat-message.khoj {
|
|
margin-left: auto;
|
|
text-align: left;
|
|
}
|
|
/* move message by you to right */
|
|
.chat-message.you {
|
|
margin-right: auto;
|
|
text-align: right;
|
|
}
|
|
/* basic style chat message text */
|
|
.chat-message-text {
|
|
margin: 10px;
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
position: relative;
|
|
display: inline-block;
|
|
max-width: 80%;
|
|
text-align: left;
|
|
}
|
|
/* color chat bubble by khoj blue */
|
|
.chat-message-text.khoj {
|
|
color: var(--primary-inverse);
|
|
background: var(--primary);
|
|
margin-left: auto;
|
|
}
|
|
/* Spinner symbol when the chat message is loading */
|
|
.spinner {
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid var(--primary-inverse);
|
|
border-radius: 50%;
|
|
width: 12px;
|
|
height: 12px;
|
|
animation: spin 2s linear infinite;
|
|
margin: 0px 0px 0px 10px;
|
|
display: inline-block;
|
|
}
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
/* add left protrusion to khoj chat bubble */
|
|
.chat-message-text.khoj:after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -2px;
|
|
left: -7px;
|
|
border: 10px solid transparent;
|
|
border-top-color: var(--primary);
|
|
border-bottom: 0;
|
|
transform: rotate(-60deg);
|
|
}
|
|
/* color chat bubble by you dark grey */
|
|
.chat-message-text.you {
|
|
color: #f8fafc;
|
|
background: #475569;
|
|
margin-right: auto;
|
|
}
|
|
/* add right protrusion to you chat bubble */
|
|
.chat-message-text.you:after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 91%;
|
|
right: -2px;
|
|
border: 10px solid transparent;
|
|
border-left-color: #475569;
|
|
border-right: 0;
|
|
margin-top: -10px;
|
|
transform: rotate(-60deg)
|
|
}
|
|
img.text-to-image {
|
|
max-width: 60%;
|
|
}
|
|
|
|
#chat-footer {
|
|
padding: 0;
|
|
margin: 8px;
|
|
display: grid;
|
|
grid-template-columns: minmax(70px, 100%);
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
}
|
|
#input-row {
|
|
display: grid;
|
|
grid-template-columns: 32px auto 32px 40px;
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
background: #f9fafc;
|
|
align-items: center;
|
|
background-color: var(--background-color);
|
|
}
|
|
.option:hover {
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
#chat-input {
|
|
font-family: var(--font-family);
|
|
font-size: small;
|
|
height: 36px;
|
|
border-radius: 16px;
|
|
resize: none;
|
|
overflow-y: hidden;
|
|
max-height: 200px;
|
|
box-sizing: border-box;
|
|
padding: 7px 0 0 12px;
|
|
line-height: 1.5em;
|
|
margin: 0;
|
|
}
|
|
#chat-input:focus {
|
|
outline: none !important;
|
|
}
|
|
.input-row-button {
|
|
background: var(--background-color);
|
|
border: none;
|
|
box-shadow: none;
|
|
border-radius: 50%;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
padding: 0;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease-in-out;
|
|
width: 40px;
|
|
height: 40px;
|
|
margin-top: -2px;
|
|
margin-left: -5px;
|
|
}
|
|
|
|
.side-panel-button {
|
|
background: var(--background-color);
|
|
border: none;
|
|
box-shadow: none;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease-in-out;
|
|
border-radius: 5%;;
|
|
font-family: var(--font-family);
|
|
padding: 8px;
|
|
font-size: large;
|
|
}
|
|
|
|
svg.side-panel-collapse {
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
|
|
.side-panel-button:hover,
|
|
.input-row-button:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.side-panel-button:active,
|
|
.input-row-button:active {
|
|
background: var(--primary-active);
|
|
}
|
|
.input-row-button-img {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
#send-button {
|
|
padding: 0;
|
|
position: relative;
|
|
}
|
|
#send-button-img {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: var(--primary-hover);
|
|
border-radius: 50%;
|
|
}
|
|
#stop-send-button-img {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 6px;
|
|
width: 28px;
|
|
height: 28px;
|
|
transform: rotateY(-180deg) rotateZ(-90deg);
|
|
}
|
|
#countdown-circle {
|
|
stroke-dasharray: 44px; /* The circumference of the circle with 7px radius */
|
|
stroke-dashoffset: 0px;
|
|
stroke-linecap: round;
|
|
stroke-width: 1px;
|
|
stroke: var(--main-text-color);
|
|
fill: none;
|
|
}
|
|
@keyframes countdown {
|
|
from {
|
|
stroke-dashoffset: 0px;
|
|
}
|
|
to {
|
|
stroke-dashoffset: -44px; /* The circumference of the circle with 7px radius */
|
|
}
|
|
}
|
|
|
|
.option-enabled {
|
|
box-shadow: 0 0 12px rgb(119, 156, 46);
|
|
}
|
|
|
|
div.collapsed {
|
|
display: none;
|
|
}
|
|
|
|
div.expanded {
|
|
display: block;
|
|
}
|
|
|
|
div.reference {
|
|
display: grid;
|
|
grid-template-rows: auto;
|
|
grid-auto-flow: row;
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
margin: 10px;
|
|
}
|
|
|
|
div.expanded.reference-section {
|
|
display: grid;
|
|
grid-template-rows: auto;
|
|
grid-auto-flow: row;
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
margin: 10px;
|
|
}
|
|
|
|
div#question-starters {
|
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
|
grid-column-gap: 8px;
|
|
}
|
|
|
|
button.question-starter {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 16px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease-in-out;
|
|
text-align: left;
|
|
max-height: 75px;
|
|
transition: max-height 0.3s ease-in-out;
|
|
overflow: hidden;
|
|
}
|
|
button.question-starter:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
code.chat-response {
|
|
background: var(--primary-hover);
|
|
color: var(--primary-inverse);
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
}
|
|
|
|
button.reference-button {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease-in-out;
|
|
text-align: left;
|
|
max-height: 75px;
|
|
transition: max-height 0.3s ease-in-out;
|
|
overflow: hidden;
|
|
}
|
|
button.reference-button.expanded {
|
|
max-height: none;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
button.reference-button::before {
|
|
content: "▶";
|
|
margin-right: 5px;
|
|
display: inline-block;
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
|
|
button.reference-button:active:before,
|
|
button.reference-button[aria-expanded="true"]::before {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
button.reference-expand-button {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: 1px dotted var(--main-text-color);
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.4s ease-in-out;
|
|
text-align: left;
|
|
}
|
|
|
|
button.reference-expand-button:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.option-enabled:focus {
|
|
outline: none !important;
|
|
border:1px solid #475569;
|
|
box-shadow: 0 0 16px var(--primary);
|
|
}
|
|
|
|
a.inline-chat-link {
|
|
color: #475569;
|
|
text-decoration: none;
|
|
border-bottom: 1px dotted #475569;
|
|
}
|
|
|
|
a.reference-link {
|
|
color: var(--main-text-color);
|
|
border-bottom: 1px dotted var(--main-text-color);
|
|
}
|
|
|
|
button.copy-button {
|
|
display: block;
|
|
border-radius: 4px;
|
|
background-color: var(--background-color);
|
|
}
|
|
button.copy-button:hover {
|
|
background: #f5f5f5;
|
|
cursor: pointer;
|
|
}
|
|
|
|
pre {
|
|
text-wrap: unset;
|
|
}
|
|
|
|
div.khoj-empty-container {
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
@media (pointer: coarse), (hover: none) {
|
|
abbr[title] {
|
|
position: relative;
|
|
padding-left: 4px; /* space references out to ease tapping */
|
|
}
|
|
abbr[title]:focus:after {
|
|
content: attr(title);
|
|
|
|
/* position tooltip */
|
|
position: absolute;
|
|
left: 16px; /* open tooltip to right of ref link, instead of on top of it */
|
|
width: auto;
|
|
z-index: 1; /* show tooltip above chat messages */
|
|
|
|
/* style tooltip */
|
|
background-color: #aaa;
|
|
color: #f8fafc;
|
|
border-radius: 2px;
|
|
box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.4);
|
|
font-size: small;
|
|
padding: 2px 4px;
|
|
}
|
|
}
|
|
@media only screen and (max-width: 600px) {
|
|
body {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto auto minmax(80px, 100%) auto;
|
|
}
|
|
body > * {
|
|
grid-column: 1;
|
|
}
|
|
#chat-footer {
|
|
padding: 0;
|
|
margin: 4px;
|
|
grid-template-columns: auto;
|
|
}
|
|
img.text-to-image {
|
|
max-width: 100%;
|
|
}
|
|
#clear-chat-button {
|
|
margin-left: 0;
|
|
}
|
|
|
|
div#side-panel.collapsed {
|
|
width: 0px;
|
|
display: block;
|
|
overflow: hidden;
|
|
padding: 0;
|
|
}
|
|
|
|
svg.side-panel-collapse {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
#chat-body-wrapper {
|
|
min-width: 0;
|
|
}
|
|
|
|
div#chat-section-wrapper {
|
|
padding: 4px;
|
|
margin: 4px;
|
|
grid-column-gap: 4px;
|
|
}
|
|
div#collapse-side-panel {
|
|
align-self: center;
|
|
padding: 0px;
|
|
}
|
|
}
|
|
@media only screen and (min-width: 600px) {
|
|
body {
|
|
grid-template-columns: auto min(90vw, 100%) auto;
|
|
grid-template-rows: auto auto minmax(80px, 100%) auto;
|
|
}
|
|
body > * {
|
|
grid-column: 2;
|
|
}
|
|
}
|
|
|
|
div#chat-tooltip {
|
|
text-align: left;
|
|
font-size: medium;
|
|
}
|
|
|
|
svg.new-convo-button {
|
|
width: 20px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
div#new-conversation {
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--main-text-color);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
button#new-conversation-button {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
div.conversation-button {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease-in-out;
|
|
text-align: left;
|
|
display: flex;
|
|
position: relative;
|
|
}
|
|
|
|
.three-dot-menu {
|
|
display: none;
|
|
/* background: var(--background-color); */
|
|
/* border: 1px solid var(--main-text-color); */
|
|
border-radius: 5px;
|
|
/* position: relative; */
|
|
position: absolute;
|
|
right: 4;
|
|
top: 4;
|
|
}
|
|
|
|
button.three-dot-menu-button-item {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: none;
|
|
box-shadow: none;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease-in-out;
|
|
font-family: var(--font-family);
|
|
border-radius: 4px;
|
|
right: 0;
|
|
}
|
|
|
|
button.three-dot-menu-button-item:hover {
|
|
background: var(--primary-hover);
|
|
color: var(--primary-inverse);
|
|
}
|
|
|
|
.three-dot-menu-button {
|
|
background: var(--background-color);
|
|
border: none;
|
|
box-shadow: none;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease-in-out;
|
|
font-family: var(--font-family);
|
|
border-radius: 4px;
|
|
right: 0;
|
|
}
|
|
|
|
.conversation-button:hover .three-dot-menu {
|
|
display: block;
|
|
}
|
|
|
|
div.conversation-menu {
|
|
position: absolute;
|
|
z-index: 1;
|
|
top: 100%;
|
|
right: 0;
|
|
text-align: right;
|
|
background-color: var(--background-color);
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
|
|
div.conversation-button:hover {
|
|
background: var(--primary-hover);
|
|
color: var(--primary-inverse);
|
|
}
|
|
|
|
div.selected-conversation {
|
|
background: var(--primary-hover) !important;
|
|
color: var(--primary-inverse) !important;
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
a.khoj-logo {
|
|
text-align: center;
|
|
}
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
|
|
div.programmatic-output {
|
|
background-color: #f5f5f5;
|
|
border: 1px solid #ddd;
|
|
border-radius: 3px;
|
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
|
color: #333;
|
|
font-family: monospace;
|
|
font-size: small;
|
|
line-height: 1.5;
|
|
margin: 10px 0;
|
|
overflow-x: auto;
|
|
padding: 10px;
|
|
white-space: pre-wrap;
|
|
}
|
|
</style>
|
|
</html>
|