mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-03 05:29:12 +00:00
Using system clock to infer user timezone on clients makes Khoj more robust to provide location aware responses. Previously only ip based location was used to infer timezone via API. This didn't provide any decent fallback when calls to ipapi failed or Khoj was being run in offline mode
441 lines
15 KiB
HTML
441 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 countryCode = null;
|
|
let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
fetch("https://ipapi.co/json")
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
city = data.city;
|
|
region = data.region;
|
|
countryName = data.country_name;
|
|
countryCode = data.country_code;
|
|
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 }),
|
|
...(!!countryCode && { country_code: countryCode }),
|
|
...(!!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>
|