mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-02 21:19:12 +00:00
* Update the conversation_id primary key field to be a uuid - update associated API endpoints - this is to improve the overall application health, by obfuscating some information about the internal database - conversation_id type is now implicitly a string, rather than an int - ensure automations are also migrated in place, such that the conversation_ids they're pointing to are now mapped to the new IDs * Update client-side API calls to correctly query with a string field * Allow modifying of conversation properties from the chat title * Improve drag and drop file experience for chat input area * Use a phosphor icon for the copy to clipboard experience for code snippets * Update conversation_id parameter to be a str type * If django_apscheduler is not in the environment, skip the migration script * Fix create automation flow by storing conversation id as string The new UUID used for conversation id can't be directly serialized. Convert to string for serializing it for later execution --------- Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
439 lines
15 KiB
HTML
439 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Khoj Mini</title>
|
|
<style>
|
|
#title-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 20px;
|
|
background-color: #f9f5de;
|
|
color: black;
|
|
-webkit-app-region: drag;
|
|
z-index: 9999;
|
|
margin: 0;
|
|
padding-left: 7px;
|
|
padding-right: 7px;
|
|
padding-top: 5px;
|
|
padding-bottom: 5px;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
}
|
|
#loading-dots {
|
|
padding-top: 10px;
|
|
text-align: center;
|
|
font-size: 16px;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
#styled-input {
|
|
width: 100%;
|
|
font-size: 14px;
|
|
height: 100px;
|
|
min-height: 50px;
|
|
background-color: #475569; /* Blue background */
|
|
color: #dcdfe4; /* White text */
|
|
font-family: 'Noto Sans', sans-serif;
|
|
border: none;
|
|
resize: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.chat-input {
|
|
margin-top: 10px;
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 5;
|
|
width: 100%;
|
|
max-width: 600px;
|
|
display: flex;
|
|
padding-top: 10px;
|
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
|
}
|
|
#input-container {
|
|
background-color: #475569;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
margin-top: 50px;
|
|
}
|
|
#send-button {
|
|
border: none;
|
|
border-radius: 5px 5px 5px 5px;
|
|
margin-top: 5px;
|
|
background-color: #5a6b84;
|
|
color: white;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
/* font-weight: bold; */
|
|
position: relative;
|
|
margin-right: 10px;
|
|
}
|
|
#send-button:hover {
|
|
background: #7489a9;
|
|
}
|
|
|
|
#edit-button {
|
|
border: none;
|
|
border-radius: 5px 5px 5px 5px;
|
|
margin-top: 5px;
|
|
background-color: #5a6b84;
|
|
color: white;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
/* font-weight: bold; */
|
|
position: relative;
|
|
}
|
|
#edit-button:hover {
|
|
background: #7489a9;
|
|
}
|
|
|
|
::-webkit-scrollbar {
|
|
width: 5px; /* Width of the scrollbar */
|
|
height: 5px;
|
|
}
|
|
/* * {
|
|
outline: 1px solid rgb(255, 255, 255);
|
|
} */
|
|
|
|
/* Track */
|
|
::-webkit-scrollbar-track {
|
|
background: #f1f1f1; /* Background of the scrollbar track */
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* Handle */
|
|
::-webkit-scrollbar-thumb {
|
|
background: #2d2d2d; /* Color of the scrollbar thumb */
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* Handle on hover */
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #000000; /* Color of the scrollbar thumb on hover */
|
|
}
|
|
|
|
#copy-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 5px;
|
|
}
|
|
#reference-expand-button{
|
|
background-color: #000000;
|
|
color: #dcdfe4; /* White text */
|
|
font-family: 'Noto Sans', sans-serif;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
border: none;
|
|
padding: 5px;
|
|
margin: 5px;
|
|
}
|
|
|
|
#linker-button{
|
|
background-color: #fee285;
|
|
color: black; /* White text */
|
|
font-family: 'Noto Sans', sans-serif;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
border: none;
|
|
padding: 5px;
|
|
margin: 5px;
|
|
}
|
|
#linker-button:hover {
|
|
background-color: #f9f5de;
|
|
}
|
|
|
|
|
|
div.collapsed {
|
|
display: none;
|
|
}
|
|
|
|
div.expanded {
|
|
display: block;
|
|
}
|
|
/* CSS for the container */
|
|
.logo-container {
|
|
/* display: flex; Use flexbox to align items */
|
|
align-items: center; /* Align items vertically */
|
|
justify-content: flex;
|
|
padding: 10px; /* Add padding to the container */
|
|
}
|
|
|
|
/* CSS for the image */
|
|
img {
|
|
width: 100px; /* Set the desired width */
|
|
height: auto; /* Allows the image to scale proportionally */
|
|
}
|
|
.clipboardText {
|
|
font-size: 14px;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
background-color: #475569;
|
|
color: #dcdfe4;
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
width: 100%;
|
|
word-wrap: break-word;
|
|
}
|
|
.reference-button {
|
|
background-color: #dde5f0;
|
|
color: #dcdfe4;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
border: none;
|
|
padding: 5px;
|
|
margin: 5px;
|
|
}
|
|
#chat-body {
|
|
width: 100%;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
word-wrap: break-word;
|
|
line-height: 20px;
|
|
}
|
|
b {
|
|
font-size: 14px;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
}
|
|
h1 {
|
|
font-size: 20px;
|
|
font-family: 'Noto Sans', sans-serif;
|
|
text-align: center;
|
|
}
|
|
body {
|
|
background-color: #ffffff;
|
|
font-family:'Noto Sans', sans-serif;
|
|
font-weight: 400;
|
|
scrollbar-width: 5px; /* "auto" or "thin" */
|
|
scrollbar-color: white white;/* thumb color and track color */
|
|
height: 300px;
|
|
overflow: scroll;
|
|
}
|
|
#chat-body-wrapper {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
@keyframes bounce {
|
|
0%, 100% {
|
|
transform: translateY(0);
|
|
}
|
|
50% {
|
|
transform: translateY(-10px);
|
|
}
|
|
}
|
|
|
|
#loading-dots span {
|
|
display: inline-block;
|
|
animation: bounce 0.9s infinite alternate;
|
|
}
|
|
|
|
.dot1 {
|
|
animation-delay: 0.1s;
|
|
}
|
|
|
|
.dot2 {
|
|
animation-delay: 0.2s;
|
|
}
|
|
|
|
.dot3 {
|
|
animation-delay: 0.3s;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="title-bar">Khoj (Esc to Quit)</div>
|
|
<div id="chat-body-wrapper">
|
|
<div id="input-container">
|
|
<textarea id="styled-input" name="styled-input">Hello World!</textarea>
|
|
<script>
|
|
try {
|
|
if (!window.clipboardAPI) {
|
|
throw new Error('clipboardAPI is not available');
|
|
}
|
|
|
|
window.clipboardAPI.sendClipboardText((clipboardText) => {
|
|
try {
|
|
const styledInput = document.getElementById('styled-input');
|
|
if (!styledInput) {
|
|
throw new Error('styled-input element not found');
|
|
}
|
|
styledInput.value = clipboardText;
|
|
console.log("success: ", clipboardText);
|
|
} catch (error) {
|
|
console.error('Error handling clipboard text:', error);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Error setting up clipboard listener:', error);
|
|
}
|
|
</script>
|
|
<div style="display: flex;">
|
|
<button id="send-button" onclick="chat()">
|
|
Send
|
|
<svg style="margin-left: 3px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send">
|
|
<path d="M5 12l10 0-5-5m5 5-5 5" />
|
|
</svg>
|
|
</button>
|
|
<button id="edit-button" onclick="edit()">
|
|
Edit
|
|
<svg style="margin-left: 6px; margin-right: 6px;" fill="#fff" height="11px" width="11px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 512 512">
|
|
<g>
|
|
<g>
|
|
<path d="m455.1,137.9l-32.4,32.4-81-81.1 32.4-32.4c6.6-6.6 18.1-6.6 24.7,0l56.3,56.4c6.8,6.8 6.8,17.9 0,24.7zm-270.7,271l-81-81.1 209.4-209.7 81,81.1-209.4,209.7zm-99.7-42l60.6,60.7-84.4,23.8 23.8-84.5zm399.3-282.6l-56.3-56.4c-11-11-50.7-31.8-82.4,0l-285.3,285.5c-2.5,2.5-4.3,5.5-5.2,8.9l-43,153.1c-2,7.1 0.1,14.7 5.2,20 5.2,5.3 15.6,6.2 20,5.2l153-43.1c3.4-0.9 6.4-2.7 8.9-5.2l285.1-285.5c22.7-22.7 22.7-59.7 0-82.5z"/>
|
|
</g>
|
|
</g>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="loading-dots" style="display: none;"></div>
|
|
<div id="chat-body"></div>
|
|
</div>
|
|
<script src="main.js"></script>
|
|
<script type="text/javascript" src="./assets/purify.min.js?v={{ khoj_version }}"></script>
|
|
<script type="text/javascript" src="./assets/markdown-it.min.js"></script>
|
|
<script src="./utils.js"></script>
|
|
<script src="./chatutils.js"></script>
|
|
<script>
|
|
|
|
let region = null;
|
|
let city = null;
|
|
let countryName = null;
|
|
let timezone = null;
|
|
|
|
fetch("https://ipapi.co/json")
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
region = data.region;
|
|
city = data.city;
|
|
countryName = data.country_name;
|
|
timezone = data.timezone;
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
return;
|
|
});
|
|
|
|
function toggleLoading() {
|
|
var dots = document.getElementById('loading-dots');
|
|
if (dots.style.display === 'none') {
|
|
dots.innerHTML = 'Loading<span class="dot1">.</span><span class="dot2">.</span><span class="dot3">.</span>';
|
|
dots.style.display = 'inline-block';
|
|
} else {
|
|
dots.innerHTML = '';
|
|
dots.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function edit() {
|
|
//enable input for text area
|
|
let inp = document.getElementById("styled-input");
|
|
inp.removeAttribute('readonly');
|
|
//put focus on text area
|
|
inp.focus();
|
|
}
|
|
|
|
async function chat(isVoice=false) {
|
|
//set chat body to empty
|
|
let chatBody = document.getElementById("chat-body");
|
|
chatBody.innerHTML = "";
|
|
toggleLoading();
|
|
let inp = document.getElementById("styled-input");
|
|
query = inp.value;
|
|
inp.setAttribute('readonly', true);
|
|
let resultsCount = localStorage.getItem("khojResultsCount") || 5;
|
|
console.log(`Query: ${query}`);
|
|
|
|
// Short circuit on empty query
|
|
if (query.length === 0)
|
|
return;
|
|
|
|
let chat_body = document.getElementById("chat-body");
|
|
|
|
let conversationID = chat_body.dataset.conversationId;
|
|
let hostURL = await window.hostURLAPI.getURL();
|
|
const khojToken = await window.tokenAPI.getToken();
|
|
const headers = { 'Authorization': `Bearer ${khojToken}`, 'Content-Type': 'application/json' };
|
|
|
|
if (!conversationID) {
|
|
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
|
|
let data = await response.json();
|
|
conversationID = data.conversation_id;
|
|
chat_body.dataset.conversationId = conversationID;
|
|
}
|
|
|
|
let newResponseEl = document.createElement("div");
|
|
newResponseEl.classList.add("chat-message", "khoj");
|
|
newResponseEl.attributes["data-meta"] = "🏮 Khoj at " + formatDate(new Date());
|
|
chat_body.appendChild(newResponseEl);
|
|
|
|
let newResponseTextEl = document.createElement("div");
|
|
newResponseTextEl.classList.add("chat-message-text", "khoj");
|
|
newResponseEl.appendChild(newResponseTextEl);
|
|
|
|
// Temporary status message to indicate that Khoj is thinking
|
|
let loadingEllipsis = createLoadingEllipsis();
|
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
|
|
toggleLoading();
|
|
|
|
// Setup chat message state
|
|
chatMessageState = {
|
|
newResponseTextEl,
|
|
newResponseEl,
|
|
loadingEllipsis,
|
|
references: {},
|
|
rawResponse: "",
|
|
rawQuery: query,
|
|
isVoice: isVoice,
|
|
}
|
|
|
|
// Construct API URL to execute chat query
|
|
const chatApi = `${hostURL}/api/chat?client=desktop`;
|
|
const chatApiBody = {
|
|
q: query,
|
|
conversation_id: conversationID,
|
|
stream: true,
|
|
...(!!city && { city: city }),
|
|
...(!!region && { region: region }),
|
|
...(!!countryName && { country: countryName }),
|
|
...(!!timezone && { timezone: timezone }),
|
|
};
|
|
|
|
const response = await fetch(chatApi, {
|
|
method: "POST",
|
|
headers: headers,
|
|
body: JSON.stringify(chatApiBody),
|
|
});
|
|
|
|
try {
|
|
if (!response.ok) throw new Error(response.statusText);
|
|
if (!response.body) throw new Error("Response body is empty");
|
|
// Stream and render chat response
|
|
await readChatStream(response);
|
|
} catch (err) {
|
|
console.error(`Khoj chat response failed with\n${err}`);
|
|
if (chatMessageState.newResponseEl.getElementsByClassName("lds-ellipsis").length > 0 && chatMessageState.loadingEllipsis)
|
|
chatMessageState.newResponseTextEl.removeChild(chatMessageState.loadingEllipsis);
|
|
let errorMsg = "Sorry, unable to get response from Khoj backend ❤️🩹. Retry or contact developers for help at <a href=mailto:'team@khoj.dev'>team@khoj.dev</a> or <a href='https://discord.gg/BDgyabRM6e'>on Discord</a>";
|
|
newResponseTextEl.textContent = errorMsg;
|
|
}
|
|
document.body.scrollTop = document.getElementById("chat-body").scrollHeight;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|