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