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 = ''; + 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} - - Copy API Key - Delete API Key - - - `; + + // 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 = ` - - - - - - - - `; + + // 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}"