mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-12-20 11:27:46 +00:00
63675b3299
- Use icons to style speech to text recording state
811 lines
32 KiB
HTML
811 lines
32 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 src="./utils.js"></script>
|
|
|
|
<script>
|
|
let chatOptions = [];
|
|
function copyProgrammaticOutput(event) {
|
|
// Remove the first 4 characters which are the "Copy" button
|
|
const programmaticOutput = event.target.parentNode.textContent.trim().slice(4);
|
|
navigator.clipboard.writeText(programmaticOutput).then(() => {
|
|
console.log("Programmatic output copied to clipboard");
|
|
}).catch((error) => {
|
|
console.error("Error copying programmatic output to clipboard:", error);
|
|
});
|
|
}
|
|
|
|
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.innerHTML = short_ref;
|
|
referenceButton.id = `ref-${index}`;
|
|
referenceButton.classList.add("reference-button");
|
|
referenceButton.classList.add("collapsed");
|
|
referenceButton.tabIndex = 0;
|
|
|
|
// Add event listener to toggle full reference on click
|
|
referenceButton.addEventListener('click', function() {
|
|
console.log(`Toggling ref-${index}`)
|
|
if (this.classList.contains("collapsed")) {
|
|
this.classList.remove("collapsed");
|
|
this.classList.add("expanded");
|
|
this.innerHTML = escaped_ref;
|
|
} else {
|
|
this.classList.add("collapsed");
|
|
this.classList.remove("expanded");
|
|
this.innerHTML = short_ref;
|
|
}
|
|
});
|
|
|
|
return referenceButton;
|
|
}
|
|
|
|
function renderMessage(message, by, dt=null, annotations=null) {
|
|
let message_time = formatDate(dt ?? new Date());
|
|
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
|
|
let formattedMessage = formatHTMLMessage(message);
|
|
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}`;
|
|
let textNode = document.createTextNode(formattedMessage);
|
|
chatMessageText.appendChild(textNode);
|
|
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;
|
|
}
|
|
|
|
function renderMessageWithReference(message, by, context=null, dt=null) {
|
|
if (context == null || context.length == 0) {
|
|
renderMessage(message, by, dt);
|
|
return;
|
|
}
|
|
|
|
let references = document.createElement('div');
|
|
|
|
let referenceExpandButton = document.createElement('button');
|
|
referenceExpandButton.classList.add("reference-expand-button");
|
|
let expandButtonText = context.length == 1 ? "1 reference" : `${context.length} references`;
|
|
referenceExpandButton.innerHTML = expandButtonText;
|
|
|
|
references.appendChild(referenceExpandButton);
|
|
|
|
let referenceSection = document.createElement('div');
|
|
referenceSection.classList.add("reference-section");
|
|
referenceSection.classList.add("collapsed");
|
|
|
|
referenceExpandButton.addEventListener('click', function() {
|
|
if (referenceSection.classList.contains("collapsed")) {
|
|
referenceSection.classList.remove("collapsed");
|
|
referenceSection.classList.add("expanded");
|
|
} else {
|
|
referenceSection.classList.add("collapsed");
|
|
referenceSection.classList.remove("expanded");
|
|
}
|
|
});
|
|
|
|
references.classList.add("references");
|
|
if (context) {
|
|
for (let index in context) {
|
|
let reference = context[index];
|
|
let polishedReference = generateReference(reference, index);
|
|
referenceSection.appendChild(polishedReference);
|
|
}
|
|
}
|
|
references.appendChild(referenceSection);
|
|
|
|
renderMessage(message, by, dt, references);
|
|
}
|
|
|
|
function formatHTMLMessage(htmlMessage) {
|
|
// Replace any ``` with <div class="programmatic-output">
|
|
let newHTML = htmlMessage.replace(/```([\s\S]*?)```/g, '<div class="programmatic-output"><button class="copy-button" onclick="copyProgrammaticOutput(event)">Copy</button>$1</div>');
|
|
// Replace any ** with <b> and __ with <u>
|
|
newHTML = newHTML.replace(/\*\*([\s\S]*?)\*\*/g, '<b>$1</b>');
|
|
newHTML = newHTML.replace(/__([\s\S]*?)__/g, '<u>$1</u>');
|
|
// 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, '');
|
|
return newHTML;
|
|
}
|
|
|
|
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 hostURL = await window.hostURLAPI.getURL();
|
|
|
|
// Generate backend API URL to execute query
|
|
let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true`;
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
|
|
|
let chat_body = document.getElementById("chat-body");
|
|
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 which returns a streamed response of type text/plain
|
|
fetch(url, { headers })
|
|
.then(response => {
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let references = null;
|
|
|
|
function readStream() {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
// Evaluate the contents of new_response_text.innerHTML after all the data has been streamed
|
|
const currentHTML = newResponseText.innerHTML;
|
|
newResponseText.innerHTML = formatHTMLMessage(currentHTML);
|
|
newResponseText.appendChild(references);
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
return;
|
|
}
|
|
|
|
// Decode message chunk from stream
|
|
const chunk = decoder.decode(value, { stream: true });
|
|
|
|
if (chunk.includes("### compiled references:")) {
|
|
const additionalResponse = chunk.split("### compiled references:")[0];
|
|
newResponseText.innerHTML += additionalResponse;
|
|
|
|
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 expandButtonText = rawReferenceAsJson.length == 1 ? "1 reference" : `${rawReferenceAsJson.length} references`;
|
|
referenceExpandButton.innerHTML = expandButtonText;
|
|
|
|
references.appendChild(referenceExpandButton);
|
|
|
|
let referenceSection = document.createElement('div');
|
|
referenceSection.classList.add("reference-section");
|
|
referenceSection.classList.add("collapsed");
|
|
|
|
referenceExpandButton.addEventListener('click', function() {
|
|
if (referenceSection.classList.contains("collapsed")) {
|
|
referenceSection.classList.remove("collapsed");
|
|
referenceSection.classList.add("expanded");
|
|
} else {
|
|
referenceSection.classList.add("collapsed");
|
|
referenceSection.classList.remove("expanded");
|
|
}
|
|
});
|
|
|
|
rawReferenceAsJson.forEach((reference, index) => {
|
|
let polishedReference = generateReference(reference, index);
|
|
referenceSection.appendChild(polishedReference);
|
|
});
|
|
references.appendChild(referenceSection);
|
|
readStream();
|
|
} else {
|
|
// Display response from Khoj
|
|
if (newResponseText.getElementsByClassName("spinner").length > 0) {
|
|
newResponseText.removeChild(loadingSpinner);
|
|
}
|
|
|
|
newResponseText.innerHTML += chunk;
|
|
readStream();
|
|
}
|
|
|
|
// Scroll to bottom of chat window as chat response is streamed
|
|
document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
});
|
|
}
|
|
readStream();
|
|
document.getElementById("chat-input").removeAttribute("disabled");
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
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;
|
|
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}` };
|
|
|
|
fetch(`${hostURL}/api/chat/history?client=web`, { 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");
|
|
|
|
// 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 => {
|
|
// Render conversation history, if any
|
|
response.forEach(chat_log => {
|
|
renderMessageWithReference(chat_log.message, chat_log.by, chat_log.context, new Date(chat_log.created));
|
|
});
|
|
})
|
|
.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();
|
|
}
|
|
}
|
|
|
|
let mediaRecorder;
|
|
async function speechToText() {
|
|
const speakButton = document.getElementById('speak-button');
|
|
const speakButtonImg = document.getElementById('speak-button-img');
|
|
const chatInput = document.getElementById('chat-input');
|
|
|
|
const hostURL = await window.hostURLAPI.getURL();
|
|
let url = `${hostURL}/api/speak?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; })
|
|
.catch(err => err.status == 422 ? console.error("Configure speech-to-text model on server.") : console.error("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.src = './assets/icons/stop-solid.svg';
|
|
speakButtonImg.alt = 'Stop Speaking';
|
|
};
|
|
|
|
// Toggle recording
|
|
if (!mediaRecorder || mediaRecorder.state === 'inactive') {
|
|
navigator.mediaDevices
|
|
.getUserMedia({ audio: true })
|
|
.then(handleRecording)
|
|
.catch((e) => {
|
|
console.error(e);
|
|
});
|
|
} else if (mediaRecorder.state === 'recording') {
|
|
mediaRecorder.stop();
|
|
speakButtonImg.src = './assets/icons/microphone-solid.svg';
|
|
speakButtonImg.alt = 'Speak';
|
|
}
|
|
}
|
|
|
|
</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>
|
|
|
|
<!-- Chat Body -->
|
|
<div id="chat-body"></div>
|
|
|
|
<!-- Chat Footer -->
|
|
<div id="chat-footer">
|
|
<div id="chat-tooltip" style="display: none;"></div>
|
|
<div id="input-row">
|
|
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea>
|
|
<button id="speak-button" onclick="speechToText()">
|
|
<img id="speak-button-img" src="./assets/icons/microphone-solid.svg" alt="Speak"></img>
|
|
</button>
|
|
</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: roboto, karma, segoe ui, sans-serif;
|
|
font-size: small;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
}
|
|
body > * {
|
|
padding: 10px;
|
|
margin: 10px;
|
|
}
|
|
#chat-body {
|
|
font-size: small;
|
|
margin: 0px;
|
|
line-height: 20px;
|
|
overflow-y: scroll; /* Make chat body scroll to see history */
|
|
}
|
|
/* 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 0 -5px;
|
|
}
|
|
/* 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;
|
|
white-space: pre-line;
|
|
}
|
|
/* 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;
|
|
white-space: pre-line;
|
|
}
|
|
/* 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)
|
|
}
|
|
|
|
#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: auto 32px;
|
|
grid-column-gap: 10px;
|
|
grid-row-gap: 10px;
|
|
background: #f9fafc
|
|
}
|
|
.option:hover {
|
|
box-shadow: 0 0 11px #aaa;
|
|
}
|
|
#chat-input {
|
|
font-family: roboto, karma, segoe ui, sans-serif;
|
|
font-size: small;
|
|
height: 54px;
|
|
resize: none;
|
|
overflow-y: hidden;
|
|
max-height: 200px;
|
|
box-sizing: border-box;
|
|
padding: 15px;
|
|
line-height: 1.5em;
|
|
margin: 0;
|
|
}
|
|
#chat-input:focus {
|
|
outline: none !important;
|
|
}
|
|
#speak-button {
|
|
background: var(--background-color);
|
|
border: none;
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.3s ease-in-out;
|
|
}
|
|
#speak-button:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
#speak-button:active {
|
|
background: var(--primary-active);
|
|
}
|
|
#speak-button-img {
|
|
width: 24px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
button.reference-button {
|
|
background: var(--background-color);
|
|
color: var(--main-text-color);
|
|
border: 1px solid var(--main-text-color);
|
|
border-radius: 5px;
|
|
padding: 5px;
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.5em;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease-in-out;
|
|
text-align: left;
|
|
max-height: 75px;
|
|
transition: max-height 0.3s ease-in-out;
|
|
overflow: hidden;
|
|
}
|
|
button.reference-button.expanded {
|
|
max-height: none;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
@media only screen and (min-width: 600px) {
|
|
body {
|
|
grid-template-columns: auto min(70vw, 100%) auto;
|
|
grid-template-rows: auto auto minmax(80px, 100%) auto;
|
|
}
|
|
body > * {
|
|
grid-column: 2;
|
|
}
|
|
}
|
|
|
|
div#chat-tooltip {
|
|
text-align: left;
|
|
font-size: medium;
|
|
}
|
|
|
|
@keyframes gradient {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
a.khoj-logo {
|
|
text-align: center;
|
|
}
|
|
|
|
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>
|