Delete Conversation History from Web, Desktop, Obsidian Clients (#551)

Add delete button to clear conversation history from Web, Desktop and Obsidian Khoj clients

Resolves #523
This commit is contained in:
Debanjum 2023-11-25 22:24:12 -08:00 committed by GitHub
commit e0a59cff68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 210 additions and 17 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>

After

Width:  |  Height:  |  Size: 503 B

View file

@ -515,6 +515,32 @@
chat(); 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);
});
}
</script> </script>
<body> <body>
<div id="khoj-empty-container" class="khoj-empty-container"> <div id="khoj-empty-container" class="khoj-empty-container">
@ -541,7 +567,12 @@
<!-- Chat Footer --> <!-- Chat Footer -->
<div id="chat-footer"> <div id="chat-footer">
<div id="chat-tooltip" style="display: none;"></div> <div id="chat-tooltip" style="display: none;"></div>
<div id="input-row">
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea> <textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea>
<button class="input-row-button" onclick="clearConversationHistory()">
<img class="input-rown-button-img" src="./assets/icons/trash-solid.svg" alt="Clear Chat History"></img>
</button>
</div>
</div> </div>
</body> </body>
@ -655,15 +686,17 @@
#chat-footer { #chat-footer {
padding: 0; padding: 0;
margin: 8px;
display: grid; display: grid;
grid-template-columns: minmax(70px, 100%); grid-template-columns: minmax(70px, 100%);
grid-column-gap: 10px; grid-column-gap: 10px;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
#chat-footer > * { #input-row {
padding: 15px; display: grid;
border-radius: 5px; grid-template-columns: auto 32px;
border: 1px solid #475569; grid-column-gap: 10px;
grid-row-gap: 10px;
background: #f9fafc background: #f9fafc
} }
.option:hover { .option:hover {
@ -684,6 +717,26 @@
#chat-input:focus { #chat-input:focus {
outline: none !important; 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 { .option-enabled {
box-shadow: 0 0 12px rgb(119, 156, 46); box-shadow: 0 0 12px rgb(119, 156, 46);

View file

@ -1,4 +1,4 @@
import { App, Modal, request } from 'obsidian'; import { App, Modal, request, setIcon } from 'obsidian';
import { KhojSetting } from 'src/settings'; import { KhojSetting } from 'src/settings';
import fetch from "node-fetch"; import fetch from "node-fetch";
@ -38,7 +38,8 @@ export class KhojChatModal extends Modal {
await this.getChatHistory(); await this.getChatHistory();
// Add chat input field // Add chat input field
const chatInput = contentEl.createEl("input", let inputRow = contentEl.createDiv("khoj-input-row");
const chatInput = inputRow.createEl("input",
{ {
attr: { attr: {
type: "text", type: "text",
@ -50,6 +51,15 @@ export class KhojChatModal extends Modal {
}) })
chatInput.addEventListener('change', (event) => { this.result = (<HTMLInputElement>event.target).value }); chatInput.addEventListener('change', (event) => { this.result = (<HTMLInputElement>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 // Scroll to bottom of modal, till the send message input box
this.modalEl.scrollTop = this.modalEl.scrollHeight; this.modalEl.scrollTop = this.modalEl.scrollHeight;
chatInput.focus(); 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 <a href='https://discord.gg/BDgyabRM6e'>in Discord</a>") this.renderIncrementalMessage(responseElement, "Sorry, unable to get response from Khoj backend ❤️‍🩹. Contact developer for help at team@khoj.dev or <a href='https://discord.gg/BDgyabRM6e'>in Discord</a>")
} }
} }
async clearConversationHistory() {
let chatInput = <HTMLInputElement>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);
}
}
} }

View file

@ -68,7 +68,7 @@ If your plugin does not need CSS, delete this file.
} }
/* color chat bubble by khoj blue */ /* color chat bubble by khoj blue */
.khoj-chat-message-text.khoj { .khoj-chat-message-text.khoj {
color: var(--text-on-accent); color: var(--khoj-chat-dark-grey);
background: var(--khoj-chat-primary); background: var(--khoj-chat-primary);
margin-left: auto; margin-left: auto;
white-space: pre-line; white-space: pre-line;
@ -110,9 +110,12 @@ If your plugin does not need CSS, delete this file.
grid-column-gap: 10px; grid-column-gap: 10px;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
#khoj-chat-footer > * { .khoj-input-row {
padding: 15px; display: grid;
background: #f9fafc grid-template-columns: auto 32px;
grid-column-gap: 10px;
grid-row-gap: 10px;
background: var(--background-primary);
} }
#khoj-chat-input.option:hover { #khoj-chat-input.option:hover {
box-shadow: 0 0 11px var(--background-modifier-box-shadow); 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); font-size: var(--font-ui-medium);
padding: 25px 20px; 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) { @media (pointer: coarse), (hover: none) {
#khoj-chat-body.abbr[title] { #khoj-chat-body.abbr[title] {

View file

@ -233,6 +233,10 @@ class ConversationAdapters:
return await conversation.afirst() return await conversation.afirst()
return await Conversation.objects.acreate(user=user) 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 @staticmethod
def has_any_conversation_config(user: KhojUser): def has_any_conversation_config(user: KhojUser):
return ChatModelOptions.objects.filter(user=user).exists() return ChatModelOptions.objects.filter(user=user).exists()

View file

@ -468,7 +468,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; document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight;
} }
window.onload = function () { window.onload = loadChat;
function loadChat() {
fetch('/api/chat/history?client=web') fetch('/api/chat/history?client=web')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
@ -540,6 +542,28 @@ To get started, just start typing below. You can also type / to see a list of co
chat(); 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);
});
}
</script> </script>
<body> <body>
<div id="khoj-empty-container" class="khoj-empty-container"> <div id="khoj-empty-container" class="khoj-empty-container">
@ -558,7 +582,12 @@ To get started, just start typing below. You can also type / to see a list of co
<!-- Chat Footer --> <!-- Chat Footer -->
<div id="chat-footer"> <div id="chat-footer">
<div id="chat-tooltip" style="display: none;"></div> <div id="chat-tooltip" style="display: none;"></div>
<div id="input-row">
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea> <textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands, or just type your questions and hit enter."></textarea>
<button class="input-row-button" onclick="clearConversationHistory()">
<img class="input-rown-button-img" src="/static/assets/icons/trash-solid.svg" alt="Clear Chat History"></img>
</button>
</div>
</div> </div>
</body> </body>
<script> <script>
@ -791,10 +820,11 @@ To get started, just start typing below. You can also type / to see a list of co
grid-column-gap: 10px; grid-column-gap: 10px;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
#chat-footer > * { #input-row {
padding: 15px; display: grid;
border-radius: 5px; grid-template-columns: auto 32px;
border: 1px solid var(--main-text-color); grid-column-gap: 10px;
grid-row-gap: 10px;
background: #f9fafc background: #f9fafc
} }
.option:hover { .option:hover {
@ -815,6 +845,27 @@ To get started, just start typing below. You can also type / to see a list of co
#chat-input:focus { #chat-input:focus {
outline: none !important; 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 { .option-enabled {
box-shadow: 0 0 12px rgb(119, 156, 46); box-shadow: 0 0 12px rgb(119, 156, 46);

View file

@ -545,6 +545,27 @@ def chat_history(
return {"status": "ok", "response": meta_log.get("chat", [])} return {"status": "ok", "response": meta_log.get("chat", [])}
@api.delete("/chat/history")
@requires(["authenticated"])
async def clear_chat_history(
request: Request,
common: CommonQueryParams,
):
user = request.user.object
# Clear Conversation History
await ConversationAdapters.adelete_conversation_by_user(user)
update_telemetry_state(
request=request,
telemetry_type="api",
api="clear_chat_history",
**common.__dict__,
)
return {"status": "ok", "message": "Conversation history cleared"}
@api.get("/chat/options", response_class=Response) @api.get("/chat/options", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
async def chat_options( async def chat_options(