Files
khoj/src/interface/desktop/shortcut.html
Debanjum Singh Solanky 04aef362e2 Default to using system clock to infer user timezone on js clients
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
2024-09-30 07:08:12 -07:00

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>