diff --git a/pyproject.toml b/pyproject.toml
index b3b4af35a..de82586fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,7 +52,8 @@ dependencies = [
"pyyaml ~= 6.0",
"rich >= 13.3.1",
"schedule == 1.1.0",
- "sentence-transformers == 2.5.1",
+ "sentence-transformers == 3.0.1",
+ "einops == 0.8.0",
"transformers >= 4.28.0",
"torch == 2.2.2",
"uvicorn == 0.17.6",
diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts
index 8bc3c6ee9..d6b7f76f1 100644
--- a/src/interface/obsidian/src/chat_view.ts
+++ b/src/interface/obsidian/src/chat_view.ts
@@ -96,8 +96,9 @@ export class KhojChatView extends KhojPaneView {
const objectSrc = `object-src 'none';`;
const csp = `${defaultSrc} ${scriptSrc} ${connectSrc} ${styleSrc} ${imgSrc} ${childSrc} ${objectSrc}`;
- // Add CSP meta tag to the Khoj Chat modal
- document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
+ // WARNING: CSP DISABLED for now as it breaks other Obsidian plugins. Enable when can scope CSP to only Khoj plugin.
+ // CSP meta tag for the Khoj Chat modal
+ // document.head.createEl("meta", { attr: { "http-equiv": "Content-Security-Policy", "content": `${csp}` } });
// Create area for chat logs
let chatBodyEl = contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } });
@@ -1014,6 +1015,7 @@ export class KhojChatView extends KhojPaneView {
// Start the countdown timer UI
stopSendButtonImg.getElementsByTagName("circle")[0].style.animation = "countdown 3s linear 1 forwards";
+ stopSendButtonImg.getElementsByTagName("circle")[0].style.color = "var(--icon-color-active)";
// Auto send message after 3 seconds
this.sendMessageTimeout = setTimeout(() => {
@@ -1043,6 +1045,7 @@ export class KhojChatView extends KhojPaneView {
this.mediaRecorder.start();
setIcon(transcribeButton, "mic-off");
+ transcribeButton.classList.add("loading-encircle")
};
// Toggle recording
@@ -1057,6 +1060,7 @@ export class KhojChatView extends KhojPaneView {
this.mediaRecorder.stop();
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
this.mediaRecorder = undefined;
+ transcribeButton.classList.remove("loading-encircle");
setIcon(transcribeButton, "mic");
}
}
diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css
index 8e3d2c6b6..8902d3c96 100644
--- a/src/interface/obsidian/styles.css
+++ b/src/interface/obsidian/styles.css
@@ -598,6 +598,30 @@ img.copy-icon {
}
}
+/* Loading Encircle */
+.loading-encircle {
+ position: relative;
+}
+
+.loading-encircle::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 24px;
+ height: 24px;
+ margin-top: -16px;
+ margin-left: -16px;
+ border: 4px solid transparent;
+ border-top-color: var(--icon-color-active);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
@media only screen and (max-width: 600px) {
div.khoj-header {
display: grid;
diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py
index 62afdd2b2..096d14bce 100644
--- a/src/khoj/database/models/__init__.py
+++ b/src/khoj/database/models/__init__.py
@@ -215,11 +215,11 @@ class ModelType(models.TextChoices):
# Bi-encoder model of sentence-transformer type to load from HuggingFace
bi_encoder = models.CharField(max_length=200, default="thenlper/gte-small")
# Config passed to the sentence-transformer model constructor. E.g. device="cuda:0", trust_remote_server=True etc.
- bi_encoder_model_config = models.JSONField(default=dict)
+ bi_encoder_model_config = models.JSONField(default=dict, blank=True)
# Query encode configs like prompt, precision, normalize_embeddings, etc. for sentence-transformer models
- bi_encoder_query_encode_config = models.JSONField(default=dict)
+ bi_encoder_query_encode_config = models.JSONField(default=dict, blank=True)
# Docs encode configs like prompt, precision, normalize_embeddings, etc. for sentence-transformer models
- bi_encoder_docs_encode_config = models.JSONField(default=dict)
+ bi_encoder_docs_encode_config = models.JSONField(default=dict, blank=True)
# Cross-encoder model of sentence-transformer type to load from HuggingFace
cross_encoder = models.CharField(max_length=200, default="mixedbread-ai/mxbai-rerank-xsmall-v1")
# Inference server API endpoint to use for embeddings inference. Bi-encoder model should be hosted on this server
diff --git a/src/khoj/interface/web/agents.html b/src/khoj/interface/web/agents.html
index 9b2793c8e..b8ff8daee 100644
--- a/src/khoj/interface/web/agents.html
+++ b/src/khoj/interface/web/agents.html
@@ -242,18 +242,25 @@
{{ agent.name }}
-
+ src="https://assets.khoj.dev/intl-tel-input/intlTelInput.min.js">
+
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html
index cf7d1598b..ad8ced270 100644
--- a/src/khoj/interface/web/chat.html
+++ b/src/khoj/interface/web/chat.html
@@ -149,7 +149,6 @@
}
function generateOnlineReference(reference, index) {
-
// Generate HTML for Chat Reference
let title = reference.title || reference.link;
let link = reference.link;
@@ -170,7 +169,7 @@
linkElement.textContent = title;
let referenceButton = document.createElement('button');
- referenceButton.innerHTML = linkElement.outerHTML;
+ referenceButton.appendChild(linkElement);
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
@@ -181,11 +180,12 @@
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
- this.innerHTML = linkElement.outerHTML + ` ${question + snippet}`;
+ this.innerHTML = `${linkElement.outerHTML} ${question}${snippet}`;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
- this.innerHTML = linkElement.outerHTML;
+ this.innerHTML = "";
+ this.appendChild(linkElement);
}
});
@@ -578,7 +578,7 @@
let referenceExpandButton = document.createElement('button');
referenceExpandButton.classList.add("reference-expand-button");
- referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
+ referenceExpandButton.textContent = numReferences == 1 ? "1 reference" : `${numReferences} references`;
referenceExpandButton.addEventListener('click', function() {
if (referenceSection.classList.contains("collapsed")) {
@@ -888,7 +888,7 @@
if (overlayText == null) {
dropzone.classList.add('dragover');
var overlayText = document.createElement("div");
- overlayText.innerHTML = "Select file(s) or drag + drop it here to share it with Khoj";
+ overlayText.textContent = "Select file(s) or drag + drop it here to share it with Khoj";
overlayText.className = "dropzone-overlay";
overlayText.id = "dropzone-overlay";
dropzone.appendChild(overlayText);
@@ -949,7 +949,7 @@
if (overlayText != null) {
// Display loading spinner
var loadingSpinner = document.createElement("div");
- overlayText.innerHTML = "Uploading file(s) for indexing";
+ overlayText.textContent = "Uploading file(s) for indexing";
loadingSpinner.className = "spinner";
overlayText.appendChild(loadingSpinner);
}
@@ -1042,7 +1042,7 @@
if (overlayText == null) {
var overlayText = document.createElement("div");
- overlayText.innerHTML = "Drop file to share it with Khoj";
+ overlayText.textContent = "Drop file to share it with Khoj";
overlayText.className = "dropzone-overlay";
overlayText.id = "dropzone-overlay";
this.appendChild(overlayText);
@@ -1179,11 +1179,15 @@
websocket.onclose = function(event) {
websocket = null;
console.log("WebSocket is closed now.");
+ let setupWebSocketButton = document.createElement("button");
+ setupWebSocketButton.textContent = "Reconnect to Server";
+ setupWebSocketButton.onclick = setupWebSocket;
let statusDotIcon = document.getElementById("connection-status-icon");
statusDotIcon.style.backgroundColor = "red";
let statusDotText = document.getElementById("connection-status-text");
+ statusDotText.innerHTML = "";
statusDotText.style.marginTop = "5px";
- statusDotText.innerHTML = 'Reconnect to Server ';
+ statusDotText.appendChild(setupWebSocketButton);
}
websocket.onerror = function(event) {
console.log("WebSocket error observed:", event);
@@ -1434,7 +1438,7 @@
questionStarterSuggestions.innerHTML = "";
data.forEach((questionStarter) => {
let questionStarterButton = document.createElement('button');
- questionStarterButton.innerHTML = questionStarter;
+ questionStarterButton.textContent = questionStarter;
questionStarterButton.classList.add("question-starter");
questionStarterButton.addEventListener('click', function() {
questionStarterSuggestions.style.display = "none";
@@ -1606,7 +1610,7 @@
let closeButton = document.createElement('button');
closeButton.id = "close-button";
- closeButton.innerHTML = "Close";
+ closeButton.textContent = "Close";
closeButton.classList.add("close-button");
closeButton.addEventListener('click', function() {
modal.remove();
@@ -1660,7 +1664,7 @@
let threeDotMenu = document.createElement('div');
threeDotMenu.classList.add("three-dot-menu");
let threeDotMenuButton = document.createElement('button');
- threeDotMenuButton.innerHTML = "⋮";
+ threeDotMenuButton.textContent = "⋮";
threeDotMenuButton.classList.add("three-dot-menu-button");
threeDotMenuButton.addEventListener('click', function(event) {
event.stopPropagation();
@@ -1679,7 +1683,7 @@
conversationMenu.classList.add("conversation-menu");
let editTitleButton = document.createElement('button');
- editTitleButton.innerHTML = "Rename";
+ editTitleButton.textContent = "Rename";
editTitleButton.classList.add("edit-title-button");
editTitleButton.classList.add("three-dot-menu-button-item");
editTitleButton.addEventListener('click', function(event) {
@@ -1713,7 +1717,7 @@
conversationTitleInputBox.appendChild(conversationTitleInput);
let conversationTitleInputButton = document.createElement('button');
- conversationTitleInputButton.innerHTML = "Save";
+ conversationTitleInputButton.textContent = "Save";
conversationTitleInputButton.classList.add("three-dot-menu-button-item");
conversationTitleInputButton.addEventListener('click', function(event) {
event.stopPropagation();
@@ -1737,7 +1741,7 @@
threeDotMenu.appendChild(conversationMenu);
let shareButton = document.createElement('button');
- shareButton.innerHTML = "Share";
+ shareButton.textContent = "Share";
shareButton.type = "button";
shareButton.classList.add("share-conversation-button");
shareButton.classList.add("three-dot-menu-button-item");
@@ -1804,7 +1808,7 @@
let deleteButton = document.createElement('button');
deleteButton.type = "button";
- deleteButton.innerHTML = "Delete";
+ deleteButton.textContent = "Delete";
deleteButton.classList.add("delete-conversation-button");
deleteButton.classList.add("three-dot-menu-button-item");
deleteButton.addEventListener('click', function(event) {
@@ -1968,12 +1972,16 @@
}
allFiles = data;
var nofilesmessage = document.getElementsByClassName("no-files-message")[0];
+ nofilesmessage.innerHTML = "";
if(allFiles.length === 0){
- nofilesmessage.innerHTML = `How to upload files `;
+ let inlineChatLinkEl = document.createElement('a');
+ inlineChatLinkEl.className = "inline-chat-link";
+ inlineChatLinkEl.href = "https://docs.khoj.dev/category/clients/";
+ inlineChatLinkEl.textContent = "How to upload files";
+ nofilesmessage.appendChild(inlineChatLinkEl);
document.getElementsByClassName("file-toggle-button")[0].style.display = "none";
}
else{
- nofilesmessage.innerHTML = "";
document.getElementsByClassName("file-toggle-button")[0].style.display = "block";
}
})
diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html
index 88725c64c..be47660f6 100644
--- a/src/khoj/interface/web/config.html
+++ b/src/khoj/interface/web/config.html
@@ -163,10 +163,6 @@
@@ -408,7 +404,8 @@
.then(data => {
if (data.status == "ok") {
let notificationBanner = document.getElementById("notification-banner");
- notificationBanner.innerHTML = "Profile name has been updated!";
+ notificationBanner.innerHTML = "";
+ notificationBanner.textContent = "Profile name has been updated!";
notificationBanner.style.display = "block";
setTimeout(function() {
notificationBanner.style.display = "none";
@@ -420,8 +417,9 @@
function updateVoiceModel() {
const voiceModel = document.getElementById("voice-models").value;
const saveVoiceModelButton = document.getElementById("save-voice-model");
+ saveVoiceModelButton.innerHTML = "";
saveVoiceModelButton.disabled = true;
- saveVoiceModelButton.innerHTML = "Saving...";
+ saveVoiceModelButton.textContent = "Saving...";
fetch('/api/config/data/voice/model?id=' + voiceModel, {
method: 'POST',
@@ -432,18 +430,19 @@
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
- saveVoiceModelButton.innerHTML = "Save";
+ saveVoiceModelButton.textContent = "Save";
saveVoiceModelButton.disabled = false;
let notificationBanner = document.getElementById("notification-banner");
- notificationBanner.innerHTML = "Voice model has been updated!";
+ notificationBanner.innerHTML = "";
+ notificationBanner.textContent = "Voice model has been updated!";
notificationBanner.style.display = "block";
setTimeout(function() {
notificationBanner.style.display = "none";
}, 5000);
} else {
- saveVoiceModelButton.innerHTML = "Error";
+ saveVoiceModelButton.textContent = "Error";
saveVoiceModelButton.disabled = false;
}
})
@@ -453,7 +452,8 @@
const chatModel = document.getElementById("chat-models").value;
const saveModelButton = document.getElementById("save-chat-model");
saveModelButton.disabled = true;
- saveModelButton.innerHTML = "Saving...";
+ saveModelButton.innerHTML = "";
+ saveModelButton.textContent = "Saving...";
fetch('/api/config/data/conversation/model?id=' + chatModel, {
method: 'POST',
@@ -464,18 +464,19 @@
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
- saveModelButton.innerHTML = "Save";
+ saveModelButton.textContent = "Save";
saveModelButton.disabled = false;
let notificationBanner = document.getElementById("notification-banner");
- notificationBanner.innerHTML = "Conversation model has been updated!";
+ notificationBanner.innerHTML = "";
+ notificationBanner.textContent = "Conversation model has been updated!";
notificationBanner.style.display = "block";
setTimeout(function() {
notificationBanner.style.display = "none";
}, 5000);
} else {
- saveModelButton.innerHTML = "Error";
+ saveModelButton.textContent = "Error";
saveModelButton.disabled = false;
}
})
@@ -489,8 +490,9 @@
const searchModel = document.getElementById("search-models").value;
const saveSearchModelButton = document.getElementById("save-search-model");
+ saveSearchModelButton.innerHTML = "";
saveSearchModelButton.disabled = true;
- saveSearchModelButton.innerHTML = "Saving...";
+ saveSearchModelButton.textContent = "Saving...";
fetch('/api/config/data/search/model?id=' + searchModel, {
method: 'POST',
@@ -501,15 +503,16 @@
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
- saveSearchModelButton.innerHTML = "Save";
+ saveSearchModelButton.textContent = "Save";
saveSearchModelButton.disabled = false;
} else {
- saveSearchModelButton.innerHTML = "Error";
+ saveSearchModelButton.textContent = "Error";
saveSearchModelButton.disabled = false;
}
let notificationBanner = document.getElementById("notification-banner");
- notificationBanner.innerHTML = "Khoj can now better understand the language of your content! Manually sync your data from one of the Khoj clients to update your knowledge base.";
+ notificationBanner.innerHTML = "";
+ notificationBanner.textContent = "Khoj can now better understand the language of your content! Manually sync your data from one of the Khoj clients to update your knowledge base.";
notificationBanner.style.display = "block";
setTimeout(function() {
notificationBanner.style.display = "none";
@@ -607,23 +610,38 @@
})
}
- var sync = document.getElementById("sync");
- sync.addEventListener("click", function(event) {
+ function populateSyncButton() {
+ let syncIconEl = document.createElement("img");
+ syncIconEl.className = "card-icon";
+ syncIconEl.src = "/static/assets/icons/sync.svg";
+ syncIconEl.alt = "Sync";
+
+ let syncButtonTitleEl = document.createElement("h3");
+ syncButtonTitleEl.className = "card-title";
+ syncButtonTitleEl.textContent = "Sync";
+
+ return [syncButtonTitleEl, syncIconEl];
+ }
+
+ var syncButtonEl = document.getElementById("sync");
+ syncButtonEl.innerHTML = "";
+ syncButtonEl.append(...populateSyncButton());
+ syncButtonEl.addEventListener("click", function(event) {
event.preventDefault();
updateIndex(
force=true,
successText="Synced!",
errorText="Unable to sync. Raise issue on Khoj Github or Discord .",
- button=sync,
+ button=syncButtonEl,
loadingText="Syncing...",
emoji="");
});
function updateIndex(force, successText, errorText, button, loadingText, emoji) {
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
- const original_html = button.innerHTML;
button.disabled = true;
- button.innerHTML = emoji + " " + loadingText;
+ button.innerHTML = ""
+ button.textContent = emoji + " " + loadingText;
fetch('/api/update?&client=web&force=' + force, {
method: 'GET',
headers: {
@@ -640,19 +658,19 @@
document.getElementById("status").style.display = "none";
button.disabled = false;
- button.innerHTML = `✅ ${successText}`;
+ button.textContent = `✅ ${successText}`;
setTimeout(function() {
- button.innerHTML = original_html;
+ button.append(...populateSyncButton());
}, 2000);
})
.catch((error) => {
console.error('Error:', error);
- document.getElementById("status").innerHTML = emoji + " " + errorText
+ document.getElementById("status").textContent = emoji + " " + errorText
document.getElementById("status").style.display = "block";
button.disabled = false;
- button.innerHTML = '⚠️ Unsuccessful';
+ button.textContent = '⚠️ Unsuccessful';
setTimeout(function() {
- button.innerHTML = original_html;
+ button.append(...populateSyncButton());
}, 2000);
});
@@ -687,7 +705,7 @@
})
.then(response => response.json())
.then(tokenObj => {
- apiKeyList.innerHTML += generateTokenRow(tokenObj);
+ apiKeyList.appendChild(generateTokenRow(tokenObj));
});
}
@@ -696,16 +714,16 @@
navigator.clipboard.writeText(token);
// Flash the API key copied icon
const apiKeyColumn = document.getElementById(`api-key-${token}`);
- const original_html = apiKeyColumn.innerHTML;
+ const original_text = apiKeyColumn.textContent;
const copyApiKeyButton = document.getElementById(`api-key-copy-${token}`);
setTimeout(function() {
copyApiKeyButton.src = "/static/assets/icons/copy-button-success.svg";
setTimeout(() => {
copyApiKeyButton.src = "/static/assets/icons/copy-button.svg";
}, 1000);
- apiKeyColumn.innerHTML = "✅ Copied!";
+ apiKeyColumn.textContent = "✅ Copied!";
setTimeout(function() {
- apiKeyColumn.innerHTML = original_html;
+ apiKeyColumn.textContent = original_text;
}, 1000);
}, 100);
}
@@ -728,16 +746,50 @@
let tokenName = tokenObj.name;
let truncatedToken = token.slice(0, 4) + "..." + token.slice(-4);
let tokenId = `${tokenName}-${truncatedToken}`;
- return `
-
- ${tokenName}
- ${truncatedToken}
-
-
-
-
-
- `;
+
+ // Create API Key Row
+ let apiKeyItemEl = document.createElement("tr");
+ apiKeyItemEl.id = `api-key-item-${token}`;
+
+ // API Key Name Row
+ let apiKeyNameEl = document.createElement("td");
+ let apiKeyNameTextEl = document.createElement("b");
+ apiKeyNameTextEl.textContent = tokenName;
+
+ // API Key Token Row
+ let apiKeyTokenEl = document.createElement("td");
+ apiKeyTokenEl.id = `api-key-${token}`;
+ apiKeyTokenEl.textContent = truncatedToken;
+
+ // API Key Actions Row
+ let apiKeyActionsEl = document.createElement("td");
+ // Copy API Key Button
+ let copyApiKeyButtonEl = document.createElement("img");
+ copyApiKeyButtonEl.id = `api-key-copy-${token}`;
+ copyApiKeyButtonEl.className = "configured-icon api-key-action enabled";
+ copyApiKeyButtonEl.src = "/static/assets/icons/copy-button.svg";
+ copyApiKeyButtonEl.alt = "Copy API Key";
+ copyApiKeyButtonEl.title = "Copy API Key";
+ copyApiKeyButtonEl.onclick = function() {
+ copyAPIKey(token);
+ };
+ // Delete API Key Button
+ let deleteApiKeyButtonEl = document.createElement("img");
+ deleteApiKeyButtonEl.id = `api-key-delete-${token}`;
+ deleteApiKeyButtonEl.className = "configured-icon api-key-action enabled";
+ deleteApiKeyButtonEl.src = "/static/assets/icons/delete.svg";
+ deleteApiKeyButtonEl.alt = "Delete API Key";
+ deleteApiKeyButtonEl.title = "Delete API Key";
+ deleteApiKeyButtonEl.onclick = function() {
+ deleteAPIKey(token);
+ };
+
+ // Construct the API Key Row
+ apiKeyNameEl.append(apiKeyNameTextEl);
+ apiKeyActionsEl.append(copyApiKeyButtonEl, deleteApiKeyButtonEl);
+ apiKeyItemEl.append(apiKeyNameEl, apiKeyTokenEl, apiKeyActionsEl);
+
+ return apiKeyItemEl;
}
function listApiKeys() {
@@ -746,7 +798,7 @@
.then(response => response.json())
.then(tokens => {
if (!tokens?.length > 0) return;
- apiKeyList.innerHTML = tokens?.map(generateTokenRow).join("");
+ apiKeyList.append(...tokens?.map(generateTokenRow));
});
}
@@ -754,11 +806,11 @@
listApiKeys();
function getIndexedDataSize() {
- document.getElementById("indexed-data-size").innerHTML = "Calculating...";
+ document.getElementById("indexed-data-size").textContent = "Calculating...";
fetch('/api/config/index/size')
.then(response => response.json())
.then(data => {
- document.getElementById("indexed-data-size").innerHTML = data.indexed_data_size_in_mb + " MB used";
+ document.getElementById("indexed-data-size").textContent = data.indexed_data_size_in_mb + " MB used";
});
}
@@ -787,7 +839,7 @@
.catch(() => callback("us"))
},
separateDialCode: true,
- utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js",
+ utilsScript: "https://assets.khoj.dev/intl-tel-input/utils.js",
});
const errorMap = ["Invalid number", "Invalid country code", "Too short", "Too long", "Invalid number"];
@@ -858,7 +910,7 @@
phonenumberVerifyButton.addEventListener("click", () => {
console.log(iti.getValidationError());
if (iti.isValidNumber() == false) {
- phoneNumberUpdateCallback.innerHTML = "Invalid phone number: " + errorMap[iti.getValidationError()];
+ phoneNumberUpdateCallback.textContent = "Invalid phone number: " + errorMap[iti.getValidationError()];
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
@@ -875,12 +927,12 @@
.then(data => {
if (data.status == "ok") {
if (isTwilioEnabled == "True" || isTwilioEnabled == "true") {
- phoneNumberUpdateCallback.innerHTML = "OTP sent to your phone number";
+ phoneNumberUpdateCallback.textContent = "OTP sent to your phone number";
phonenumberVerifyOTPButton.style.display = "block";
phonenumberOTPInput.style.display = "block";
} else {
phonenumberVerifiedText.style.display = "block";
- phoneNumberUpdateCallback.innerHTML = "Phone number updated";
+ phoneNumberUpdateCallback.textContent = "Phone number updated";
phonenumberUnverifiedText.style.display = "none";
}
phonenumberVerifyButton.style.display = "none";
@@ -889,7 +941,7 @@
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
} else {
- phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
+ phoneNumberUpdateCallback.textContent = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
@@ -898,7 +950,7 @@
})
.catch((error) => {
console.error('Error:', error);
- phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
+ phoneNumberUpdateCallback.textContent = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
@@ -910,7 +962,7 @@
phonenumberVerifyOTPButton.addEventListener("click", () => {
const otp = phonenumberOTPInput.value;
if (otp.length != 6) {
- phoneNumberUpdateCallback.innerHTML = "Your OTP should be exactly 6 digits";
+ phoneNumberUpdateCallback.textContent = "Your OTP should be exactly 6 digits";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
@@ -927,7 +979,7 @@
.then(response => response.json())
.then(data => {
if (data.status == "ok") {
- phoneNumberUpdateCallback.innerHTML = "Phone number updated";
+ phoneNumberUpdateCallback.textContent = "Phone number updated";
phonenumberVerifiedText.style.display = "block";
phonenumberUnverifiedText.style.display = "none";
phoneNumberUpdateCallback.style.display = "block";
@@ -939,7 +991,7 @@
phoneNumberUpdateCallback.style.display = "none";
}, 5000);
} else {
- phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
+ phoneNumberUpdateCallback.textContent = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
@@ -948,7 +1000,7 @@
})
.catch((error) => {
console.error('Error:', error);
- phoneNumberUpdateCallback.innerHTML = "Error updating phone number";
+ phoneNumberUpdateCallback.textContent = "Error updating phone number";
phoneNumberUpdateCallback.style.display = "block";
setTimeout(function() {
phoneNumberUpdateCallback.style.display = "none";
diff --git a/src/khoj/interface/web/content_source_computer_input.html b/src/khoj/interface/web/content_source_computer_input.html
index 77816f353..49f8bdc51 100644
--- a/src/khoj/interface/web/content_source_computer_input.html
+++ b/src/khoj/interface/web/content_source_computer_input.html
@@ -56,7 +56,10 @@
if (data.length == 0) {
document.getElementById("delete-all-files").style.display = "none";
- indexedFiles.innerHTML = " No documents synced with Khoj
";
+ let noFilesElement = document.createElement("div");
+ noFilesElement.classList.add("card-description");
+ noFilesElement.textContent = "No documents synced with Khoj";
+ indexedFiles.appendChild(noFilesElement);
} else {
document.getElementById("get-desktop-client").style.display = "none";
document.getElementById("delete-all-files").style.display = "block";
@@ -86,14 +89,14 @@
let fileNameElement = document.createElement("div");
fileNameElement.classList.add("content-name");
- fileNameElement.innerHTML = filename;
+ fileNameElement.textContent = filename;
fileElement.appendChild(fileNameElement);
let buttonContainer = document.createElement("div");
buttonContainer.classList.add("remove-button-container");
let removeFileButton = document.createElement("button");
removeFileButton.classList.add("remove-file-button");
- removeFileButton.innerHTML = "🗑️";
+ removeFileButton.textContent = "🗑️";
removeFileButton.addEventListener("click", ((filename) => {
return () => {
removeFile(filename);
diff --git a/src/khoj/interface/web/content_source_github_input.html b/src/khoj/interface/web/content_source_github_input.html
index a7d6ccdfd..875ad5e33 100644
--- a/src/khoj/interface/web/content_source_github_input.html
+++ b/src/khoj/interface/web/content_source_github_input.html
@@ -70,18 +70,50 @@ Repositories
repo.classList.add("repo");
const id = Date.now();
repo.id = "repo-card-" + id;
- repo.innerHTML = `
- Repository Owner
-
- Repository Name
-
- Repository Branch
-
- Remove Repository
- `;
+
+ // Create repo owner, name, branch elements
+ let repoOwnerLabel = document.createElement("label");
+ repoOwnerLabel.textContent = "Repository Owner";
+ repoOwnerLabel.for = "repo-owner";
+
+ let repoOwner = document.createElement("input");
+ repoOwner.type = "text";
+ repoOwner.id = "repo-owner-" + id;
+ repoOwner.name = "repo_owner";
+
+ let repoNameLabel = document.createElement("label");
+ repoNameLabel.textContent = "Repository Name";
+ repoNameLabel.for = "repo-name";
+
+ let repoName = document.createElement("input");
+ repoName.type = "text";
+ repoName.id = "repo-name-" + id;
+ repoName.name = "repo_name";
+
+ let repoBranchLabel = document.createElement("label");
+ repoBranchLabel.textContent = "Repository Branch";
+ repoBranchLabel.for = "repo-branch";
+
+ let repoBranch = document.createElement("input");
+ repoBranch.type = "text";
+ repoBranch.id = "repo-branch-" + id;
+ repoBranch.name = "repo_branch";
+
+ let removeRepoButton = document.createElement("button");
+ removeRepoButton.type = "button";
+ removeRepoButton.classList.add("remove-repo-button");
+ removeRepoButton.onclick = function() { remove_repo(id); };
+ removeRepoButton.id = "remove-repo-button-" + id;
+ removeRepoButton.textContent = "Remove Repository";
+
+ // Append elements to repo card
+ repo.append(
+ repoOwnerLabel, repoOwner,
+ repoNameLabel, repoName,
+ repoBranchLabel, repoBranch,
+ removeRepoButton
+ );
+
document.getElementById("repositories").appendChild(repo);
})
@@ -95,7 +127,7 @@ Repositories
const pat_token = document.getElementById("pat-token").value;
if (pat_token == "") {
- document.getElementById("success").innerHTML = "❌ Please enter a Personal Access Token.";
+ document.getElementById("success").textContent = "❌ Please enter a Personal Access Token.";
document.getElementById("success").style.display = "block";
return;
}
@@ -122,14 +154,14 @@ Repositories
}
if (repos.length == 0) {
- document.getElementById("success").innerHTML = "❌ Please add at least one repository.";
+ document.getElementById("success").textContent = "❌ Please add at least one repository.";
document.getElementById("success").style.display = "block";
return;
}
const submitButton = document.getElementById("submit");
submitButton.disabled = true;
- submitButton.innerHTML = "Saving...";
+ submitButton.textContent = "Saving...";
// Save Github config on server
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
@@ -147,11 +179,11 @@ Repositories
.then(response => response.json())
.then(data => { data["status"] === "ok" ? data : Promise.reject(data) })
.catch(error => {
- document.getElementById("success").innerHTML = "⚠️ Failed to save Github settings.";
+ document.getElementById("success").textContent = "⚠️ Failed to save Github settings.";
document.getElementById("success").style.display = "block";
- submitButton.innerHTML = "⚠️ Failed to save settings";
+ submitButton.textContent = "⚠️ Failed to save settings";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
return;
@@ -163,18 +195,18 @@ Repositories
.then(data => { data["status"] == "ok" ? data : Promise.reject(data) })
.then(data => {
document.getElementById("success").style.display = "none";
- submitButton.innerHTML = "✅ Successfully updated";
+ submitButton.textContent = "✅ Successfully updated";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
})
.catch(error => {
- document.getElementById("success").innerHTML = "⚠️ Failed to save Github content.";
+ document.getElementById("success").textContent = "⚠️ Failed to save Github content.";
document.getElementById("success").style.display = "block";
- submitButton.innerHTML = "⚠️ Failed to save content";
+ submitButton.textContent = "⚠️ Failed to save content";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
});
diff --git a/src/khoj/interface/web/content_source_notion_input.html b/src/khoj/interface/web/content_source_notion_input.html
index 176da7134..d03d4b670 100644
--- a/src/khoj/interface/web/content_source_notion_input.html
+++ b/src/khoj/interface/web/content_source_notion_input.html
@@ -34,14 +34,14 @@
const token = document.getElementById("token").value;
if (token == "") {
- document.getElementById("success").innerHTML = "❌ Please enter a Notion Token.";
+ document.getElementById("success").textContent = "❌ Please enter a Notion Token.";
document.getElementById("success").style.display = "block";
return;
}
const submitButton = document.getElementById("submit");
submitButton.disabled = true;
- submitButton.innerHTML = "Syncing...";
+ submitButton.textContent = "Syncing...";
// Save Notion config on server
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
@@ -58,11 +58,11 @@
.then(response => response.json())
.then(data => { data["status"] === "ok" ? data : Promise.reject(data) })
.catch(error => {
- document.getElementById("success").innerHTML = "⚠️ Failed to save Notion settings.";
+ document.getElementById("success").textContent = "⚠️ Failed to save Notion settings.";
document.getElementById("success").style.display = "block";
- submitButton.innerHTML = "⚠️ Failed to save settings";
+ submitButton.textContent = "⚠️ Failed to save settings";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
return;
@@ -74,18 +74,18 @@
.then(data => { data["status"] == "ok" ? data : Promise.reject(data) })
.then(data => {
document.getElementById("success").style.display = "none";
- submitButton.innerHTML = "✅ Successfully updated";
+ submitButton.textContent = "✅ Successfully updated";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
})
.catch(error => {
- document.getElementById("success").innerHTML = "⚠️ Failed to save Notion content.";
+ document.getElementById("success").textContent = "⚠️ Failed to save Notion content.";
document.getElementById("success").style.display = "block";
- submitButton.innerHTML = "⚠️ Failed to save content";
+ submitButton.textContent = "⚠️ Failed to save content";
setTimeout(function() {
- submitButton.innerHTML = "Save";
+ submitButton.textContent = "Save";
submitButton.disabled = false;
}, 2000);
});
diff --git a/src/khoj/interface/web/public_conversation.html b/src/khoj/interface/web/public_conversation.html
index b483b3b7f..888d03b3a 100644
--- a/src/khoj/interface/web/public_conversation.html
+++ b/src/khoj/interface/web/public_conversation.html
@@ -127,7 +127,7 @@
linkElement.textContent = title;
let referenceButton = document.createElement('button');
- referenceButton.innerHTML = linkElement.outerHTML;
+ referenceButton.appendChild(linkElement);
referenceButton.id = `ref-${index}`;
referenceButton.classList.add("reference-button");
referenceButton.classList.add("collapsed");
@@ -138,11 +138,12 @@
if (this.classList.contains("collapsed")) {
this.classList.remove("collapsed");
this.classList.add("expanded");
- this.innerHTML = linkElement.outerHTML + ` ${question + snippet}`;
+ this.innerHTML = `${linkElement.outerHTML} ${question + snippet}`;
} else {
this.classList.add("collapsed");
this.classList.remove("expanded");
- this.innerHTML = linkElement.outerHTML;
+ this.innerHTML = "";
+ this.appendChild(linkElement);
}
});
@@ -296,7 +297,7 @@
}
let expandButtonText = numReferences == 1 ? "1 reference" : `${numReferences} references`;
- referenceExpandButton.innerHTML = expandButtonText;
+ referenceExpandButton.textContent = expandButtonText;
references.appendChild(referenceSection);
@@ -447,7 +448,7 @@
let referenceExpandButton = document.createElement('button');
referenceExpandButton.classList.add("reference-expand-button");
- referenceExpandButton.innerHTML = numReferences == 1 ? "1 reference" : `${numReferences} references`;
+ referenceExpandButton.textContent = numReferences == 1 ? "1 reference" : `${numReferences} references`;
referenceExpandButton.addEventListener('click', function() {
if (referenceSection.classList.contains("collapsed")) {
@@ -815,7 +816,7 @@
let closeButton = document.createElement('button');
closeButton.id = "close-button";
- closeButton.innerHTML = "Close";
+ closeButton.textContent = "Close";
closeButton.classList.add("close-button");
closeButton.addEventListener('click', function() {
modal.remove();
diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py
index a42838d9b..be28622b4 100644
--- a/src/khoj/routers/api_chat.py
+++ b/src/khoj/routers/api_chat.py
@@ -13,6 +13,7 @@
from starlette.websockets import WebSocketDisconnect
from websockets import ConnectionClosedOK
+from khoj.app.settings import ALLOWED_HOSTS
from khoj.database.adapters import (
ConversationAdapters,
DataStoreAdapters,
@@ -189,7 +190,17 @@ async def sendfeedback(request: Request, data: FeedbackData):
@api_chat.post("/speech")
@requires(["authenticated", "premium"])
-async def text_to_speech(request: Request, common: CommonQueryParams, text: str):
+async def text_to_speech(
+ request: Request,
+ common: CommonQueryParams,
+ text: str,
+ rate_limiter_per_minute=Depends(
+ ApiUserRateLimiter(requests=5, subscribed_requests=20, window=60, slug="chat_minute")
+ ),
+ rate_limiter_per_day=Depends(
+ ApiUserRateLimiter(requests=5, subscribed_requests=300, window=60 * 60 * 24, slug="chat_day")
+ ),
+) -> Response:
voice_model = await ConversationAdapters.aget_voice_model_config(request.user.object)
params = {"text_to_speak": text}
@@ -386,17 +397,19 @@ def duplicate_chat_history_public_conversation(
conversation_id: int,
):
user = request.user.object
+ domain = request.headers.get("host")
+ scheme = request.url.scheme
+
+ # Throw unauthorized exception if domain not in ALLOWED_HOSTS
+ host_domain = domain.split(":")[0]
+ if host_domain not in ALLOWED_HOSTS:
+ raise HTTPException(status_code=401, detail="Unauthorized domain")
# Duplicate Conversation History to Public Conversation
conversation = ConversationAdapters.get_conversation_by_user(user, request.user.client_app, conversation_id)
-
public_conversation = ConversationAdapters.make_public_conversation_copy(conversation)
-
public_conversation_url = PublicConversationAdapters.get_public_conversation_url(public_conversation)
- domain = request.headers.get("host")
- scheme = request.url.scheme
-
update_telemetry_state(
request=request,
telemetry_type="api",
diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py
index 8249d66e3..e7d28301f 100644
--- a/src/khoj/routers/auth.py
+++ b/src/khoj/routers/auth.py
@@ -42,8 +42,12 @@ class MagicLinkForm(BaseModel):
from google.oauth2 import id_token
except ImportError:
missing_requirements += ["Install the Khoj production package with `pip install khoj-assistant[prod]`"]
- if not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET"):
- missing_requirements += ["Set your GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET as environment variables"]
+ if not os.environ.get("RESEND_API_KEY") and (
+ not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET")
+ ):
+ missing_requirements += [
+ "Set your RESEND_API_KEY or GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET as environment variables"
+ ]
if missing_requirements:
requirements_string = "\n - " + "\n - ".join(missing_requirements)
error_msg = f"🚨 Start Khoj with --anonymous-mode flag or to enable authentication:{requirements_string}"