diff --git a/src/interface/desktop/assets/icons/trash-solid.svg b/src/interface/desktop/assets/icons/trash-solid.svg new file mode 100644 index 00000000..768d80f8 --- /dev/null +++ b/src/interface/desktop/assets/icons/trash-solid.svg @@ -0,0 +1 @@ + diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index ac8d9589..35bc7422 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -528,6 +528,32 @@ chat(); } } + + async function clearConversationHistory() { + let chatInput = document.getElementById("chat-input"); + let originalPlaceholder = chatInput.placeholder; + let chatBody = document.getElementById("chat-body"); + + const hostURL = await window.hostURLAPI.getURL(); + const khojToken = await window.tokenAPI.getToken(); + const headers = { 'Authorization': `Bearer ${khojToken}` }; + + fetch(`${hostURL}/api/chat/history?client=desktop`, { method: "DELETE", headers }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(data => { + chatBody.innerHTML = ""; + loadChat(); + chatInput.placeholder = "Cleared conversation history"; + }) + .catch(err => { + chatInput.placeholder = "Failed to clear conversation history"; + }) + .finally(() => { + setTimeout(() => { + chatInput.placeholder = originalPlaceholder; + }, 2000); + }); + }
@@ -554,7 +580,12 @@ @@ -668,15 +699,17 @@ #chat-footer { padding: 0; + margin: 8px; display: grid; grid-template-columns: minmax(70px, 100%); grid-column-gap: 10px; grid-row-gap: 10px; } - #chat-footer > * { - padding: 15px; - border-radius: 5px; - border: 1px solid #475569; + #input-row { + display: grid; + grid-template-columns: auto 32px; + grid-column-gap: 10px; + grid-row-gap: 10px; background: #f9fafc } .option:hover { @@ -697,6 +730,26 @@ #chat-input:focus { outline: none !important; } + .input-row-button { + background: var(--background-color); + border: none; + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.3s ease-in-out; + } + .input-row-button:hover { + background: var(--primary-hover); + } + .input-row-button:active { + background: var(--primary-active); + } + .input-row-button-img { + width: 24px; + } .option-enabled { box-shadow: 0 0 12px rgb(119, 156, 46); diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 001f7c41..773f88d8 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -98,6 +98,11 @@ :group 'khoj :type 'string) +(defcustom khoj-auto-index t + "Should content be automatically re-indexed every `khoj-index-interval' seconds." + :group 'khoj + :type 'boolean) + (defcustom khoj-index-interval 3600 "Interval (in seconds) to wait before updating content index." :group 'khoj @@ -405,14 +410,16 @@ Auto invokes setup steps on calling main entrypoint." ;; render response from indexing API endpoint on server (lambda (status) (if (not status) - (message "khoj.el: %scontent index %supdated" (if content-type (format "%s " content-type) "") (if force "force " "")) - (with-current-buffer (current-buffer) - (goto-char "\n\n") - (message "khoj.el: Failed to %supdate %s content index. Status: %s. Response: %s" - (if force "force " "") - content-type - status - (string-trim (buffer-substring-no-properties (point) (point-max))))))) + (message "khoj.el: %scontent index %supdated" (if content-type (format "%s " content-type) "all ") (if force "force " "")) + (progn + (khoj--delete-open-network-connections-to-server) + (with-current-buffer (current-buffer) + (search-forward "\n\n" nil t) + (message "khoj.el: Failed to %supdate %s content index. Status: %s%s" + (if force "force " "") + (if content-type (format "%s " content-type) "all") + (string-trim (format "%s %s" (nth 1 (nth 1 status)) (nth 2 (nth 1 status)))) + (if (> (- (point-max) (point)) 0) (format ". Response: %s" (string-trim (buffer-substring-no-properties (point) (point-max)))) "")))))) nil t t))) (setq khoj--indexed-files files-to-index))) @@ -444,8 +451,9 @@ Use `BOUNDARY' to separate files. This is sent to Khoj server as a POST request. (when khoj--index-timer (cancel-timer khoj--index-timer)) ;; Send files to index on server every `khoj-index-interval' seconds -(setq khoj--index-timer - (run-with-timer 60 khoj-index-interval 'khoj--server-index-files)) +(when khoj-auto-index + (setq khoj--index-timer + (run-with-timer 60 khoj-index-interval 'khoj--server-index-files))) ;; ------------------------------------------- @@ -858,7 +866,7 @@ RECEIVE-DATE is the message receive date." (let ((proc-buf (buffer-name (process-buffer proc))) (khoj-network-proc-buf (string-join (split-string khoj-server-url "://") " "))) (when (string-match (format "%s" khoj-network-proc-buf) proc-buf) - (delete-process proc))))) + (ignore-errors (delete-process proc)))))) (defun khoj--teardown-incremental-search () "Teardown hooks used for incremental search." diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index a8008048..fc6d5a48 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -1,4 +1,4 @@ -import { App, Modal, request } from 'obsidian'; +import { App, Modal, request, setIcon } from 'obsidian'; import { KhojSetting } from 'src/settings'; import fetch from "node-fetch"; @@ -38,7 +38,8 @@ export class KhojChatModal extends Modal { await this.getChatHistory(); // Add chat input field - const chatInput = contentEl.createEl("input", + let inputRow = contentEl.createDiv("khoj-input-row"); + const chatInput = inputRow.createEl("input", { attr: { type: "text", @@ -50,6 +51,15 @@ export class KhojChatModal extends Modal { }) chatInput.addEventListener('change', (event) => { this.result = (event.target).value }); + let clearChat = inputRow.createEl("button", { + text: "Clear History", + attr: { + class: "khoj-input-row-button", + }, + }) + clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); + setIcon(clearChat, "trash"); + // Scroll to bottom of modal, till the send message input box this.modalEl.scrollTop = this.modalEl.scrollHeight; chatInput.focus(); @@ -194,4 +204,35 @@ export class KhojChatModal extends Modal { this.renderIncrementalMessage(responseElement, "Sorry, unable to get response from Khoj backend ❤️‍🩹. Contact developer for help at team@khoj.dev or in Discord") } } + + async clearConversationHistory() { + let chatInput = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + let originalPlaceholder = chatInput.placeholder; + let chatBody = this.contentEl.getElementsByClassName("khoj-chat-body")[0]; + + let response = await request({ + url: `${this.setting.khojUrl}/api/chat/history?client=web`, + method: "DELETE", + headers: { "Authorization": `Bearer ${this.setting.khojApiKey}` }, + }) + try { + let result = JSON.parse(response); + if (result.status !== "ok") { + // Throw error if conversation history isn't cleared + throw new Error("Failed to clear conversation history"); + } else { + // If conversation history is cleared successfully, clear chat logs from modal + chatBody.innerHTML = ""; + await this.getChatHistory(); + chatInput.placeholder = result.message; + } + } catch (err) { + chatInput.placeholder = "Failed to clear conversation history"; + } finally { + // Reset to original placeholder text after some time + setTimeout(() => { + chatInput.placeholder = originalPlaceholder; + }, 2000); + } + } } diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index d322804d..95a304f1 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -68,7 +68,7 @@ If your plugin does not need CSS, delete this file. } /* color chat bubble by khoj blue */ .khoj-chat-message-text.khoj { - color: var(--text-on-accent); + color: var(--khoj-chat-dark-grey); background: var(--khoj-chat-primary); margin-left: auto; white-space: pre-line; @@ -110,9 +110,12 @@ If your plugin does not need CSS, delete this file. grid-column-gap: 10px; grid-row-gap: 10px; } -#khoj-chat-footer > * { - padding: 15px; - background: #f9fafc +.khoj-input-row { + display: grid; + grid-template-columns: auto 32px; + grid-column-gap: 10px; + grid-row-gap: 10px; + background: var(--background-primary); } #khoj-chat-input.option:hover { box-shadow: 0 0 11px var(--background-modifier-box-shadow); @@ -121,6 +124,25 @@ If your plugin does not need CSS, delete this file. font-size: var(--font-ui-medium); padding: 25px 20px; } +.khoj-input-row-button { + background: var(--background-primary); + border: none; + border-radius: 5px; + padding: 5px; + --icon-size: var(--icon-size); + height: auto; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.3s ease-in-out; +} +.khoj-input-row-button:hover { + background: var(--background-modifier-hover); +} +.khoj-input-row-button:active { + background: var(--background-modifier-active); +} @media (pointer: coarse), (hover: none) { #khoj-chat-body.abbr[title] { diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index bcf4856c..5bfab8d3 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -259,6 +259,10 @@ class ConversationAdapters: return await conversation.afirst() return await Conversation.objects.acreate(user=user) + @staticmethod + async def adelete_conversation_by_user(user: KhojUser): + return await Conversation.objects.filter(user=user).adelete() + @staticmethod def has_any_conversation_config(user: KhojUser): return ChatModelOptions.objects.filter(user=user).exists() diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index f67ab857..df39ca4f 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -482,7 +482,9 @@ To get started, just start typing below. You can also type / to see a list of co document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; } - window.onload = function () { + window.onload = loadChat; + + function loadChat() { fetch('/api/chat/history?client=web') .then(response => response.json()) .then(data => { @@ -554,6 +556,28 @@ To get started, just start typing below. You can also type / to see a list of co chat(); } } + + function clearConversationHistory() { + let chatInput = document.getElementById("chat-input"); + let originalPlaceholder = chatInput.placeholder; + let chatBody = document.getElementById("chat-body"); + + fetch(`/api/chat/history?client=web`, { method: "DELETE" }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(data => { + chatBody.innerHTML = ""; + loadChat(); + chatInput.placeholder = "Cleared conversation history"; + }) + .catch(err => { + chatInput.placeholder = "Failed to clear conversation history"; + }) + .finally(() => { + setTimeout(() => { + chatInput.placeholder = originalPlaceholder; + }, 2000); + }); + }
@@ -572,7 +596,12 @@ To get started, just start typing below. You can also type / to see a list of co