View, switch chat sessions from Obsidian chat pane

This commit is contained in:
Debanjum Singh Solanky 2024-05-28 10:15:46 +05:30
parent e86899eec4
commit 1ea7675fc9
2 changed files with 250 additions and 6 deletions

View file

@ -106,14 +106,14 @@ export class KhojChatView extends KhojPaneView {
// Add chat input field // Add chat input field
let inputRow = contentEl.createDiv("khoj-input-row"); let inputRow = contentEl.createDiv("khoj-input-row");
let clearChat = inputRow.createEl("button", { let chatSessions = inputRow.createEl("button", {
text: "Clear History", text: "Chat Sessions",
attr: { attr: {
class: "khoj-input-row-button clickable-icon", class: "khoj-input-row-button clickable-icon",
}, },
}) })
clearChat.addEventListener('click', async (_) => { await this.clearConversationHistory() }); chatSessions.addEventListener('click', async (_) => { await this.showChatSessions(chatBodyEl) });
setIcon(clearChat, "trash"); setIcon(chatSessions, "history");
let chatInput = inputRow.createEl("textarea", { let chatInput = inputRow.createEl("textarea", {
attr: { attr: {
@ -394,13 +394,13 @@ export class KhojChatView extends KhojPaneView {
return imageMarkdown; return imageMarkdown;
} }
renderMessage(chatEl: Element, message: string, sender: string, dt?: Date, raw: boolean=false, willReplace: boolean=true): Element { renderMessage(chatBodyEl: Element, message: string, sender: string, dt?: Date, raw: boolean=false, willReplace: boolean=true): Element {
let message_time = this.formatDate(dt ?? new Date()); let message_time = this.formatDate(dt ?? new Date());
let emojified_sender = sender == "khoj" ? "🏮 Khoj" : "🤔 You"; let emojified_sender = sender == "khoj" ? "🏮 Khoj" : "🤔 You";
// Append message to conversation history HTML element. // Append message to conversation history HTML element.
// The chat logs should display above the message input box to follow standard UI semantics // The chat logs should display above the message input box to follow standard UI semantics
let chatMessageEl = chatEl.createDiv({ let chatMessageEl = chatBodyEl.createDiv({
attr: { attr: {
"data-meta": `${emojified_sender} at ${message_time}`, "data-meta": `${emojified_sender} at ${message_time}`,
class: `khoj-chat-message ${sender}` class: `khoj-chat-message ${sender}`
@ -482,6 +482,169 @@ export class KhojChatView extends KhojPaneView {
return `${time_string}, ${date_string}`; return `${time_string}, ${date_string}`;
} }
createNewConversation(chatBodyEl: HTMLElement) {
chatBodyEl.innerHTML = "";
chatBodyEl.dataset.conversationId = "";
chatBodyEl.dataset.conversationTitle = "";
this.renderMessage(chatBodyEl, "Hey 👋🏾, what's up?", "khoj");
}
async showChatSessions(chatBodyEl: HTMLElement): Promise<boolean> {
chatBodyEl.innerHTML = "";
const sidePanelEl = this.contentEl.createDiv("side-panel");
const newConversationEl = sidePanelEl.createDiv("new-conversation");
const conversationHeaderTitleEl = newConversationEl.createDiv("conversation-header-title");
conversationHeaderTitleEl.textContent = "Conversations";
const newConversationButtonEl = newConversationEl.createEl("button");
newConversationButtonEl.classList.add("new-conversation-button");
newConversationButtonEl.classList.add("side-panel-button");
newConversationButtonEl.addEventListener('click', (_) => this.createNewConversation(chatBodyEl));
setIcon(newConversationButtonEl, "plus");
newConversationButtonEl.innerHTML += "New";
const existingConversationsEl = sidePanelEl.createDiv("existing-conversations");
const conversationListEl = existingConversationsEl.createDiv("conversation-list");
const conversationListBodyHeaderEl = conversationListEl.createDiv("conversation-list-header");
const conversationListBodyEl = conversationListEl.createDiv("conversation-list-body");
const chatSessionsUrl = `${this.setting.khojUrl}/api/chat/sessions?client=obsidian`;
const headers = { 'Authorization': `Bearer ${this.setting.khojApiKey}` };
try {
let response = await fetch(chatSessionsUrl, { method: "GET", headers: headers });
let responseJson: any = await response.json();
let conversationId = chatBodyEl.dataset.conversationId;
if (responseJson.length > 0) {
conversationListBodyHeaderEl.style.display = "block";
for (let key in responseJson) {
let conversation = responseJson[key];
let conversationSessionEl = this.contentEl.createEl('div');
let incomingConversationId = conversation["conversation_id"];
const conversationTitle = conversation["slug"] || `New conversation 🌱`;
conversationSessionEl.textContent = conversationTitle;
conversationSessionEl.classList.add("conversation-session");
if (incomingConversationId == conversationId) {
conversationSessionEl.classList.add("selected-conversation");
}
conversationSessionEl.addEventListener('click', () => {
chatBodyEl.innerHTML = "";
chatBodyEl.dataset.conversationId = incomingConversationId;
chatBodyEl.dataset.conversationTitle = conversationTitle;
this.getChatHistory(chatBodyEl);
});
let threeDotMenuEl = this.contentEl.createEl('div');
threeDotMenuEl.classList.add("three-dot-menu");
let threeDotMenuButton = this.contentEl.createEl('button');
threeDotMenuButton.innerHTML = "⋮";
threeDotMenuButton.classList.add("three-dot-menu-button");
threeDotMenuButton.addEventListener('click', (event) => {
event.stopPropagation();
let existingChildren = threeDotMenuEl.children;
if (existingChildren.length > 1) {
// Skip deleting the first, since that's the menu button.
for (let i = 1; i < existingChildren.length; i++) {
existingChildren[i].remove();
}
return;
}
let conversationMenuEl = this.contentEl.createEl('div');
conversationMenuEl.classList.add("conversation-menu");
let editConversationTitleButtonEl = this.contentEl.createEl('button');
editConversationTitleButtonEl.innerHTML = "Rename";
editConversationTitleButtonEl.classList.add("edit-title-button");
editConversationTitleButtonEl.classList.add("three-dot-menu-button-item");
editConversationTitleButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
let conversationMenuChildren = conversationMenuEl.children;
let totalItems = conversationMenuChildren.length;
for (let i = totalItems - 1; i >= 0; i--) {
conversationMenuChildren[i].remove();
}
// Create a dialog box to get new title for conversation
let editConversationTitleInputBoxEl = this.contentEl.createEl('div');
editConversationTitleInputBoxEl.classList.add("conversation-title-input-box");
let editConversationTitleInputEl = this.contentEl.createEl('input');
editConversationTitleInputEl.classList.add("conversation-title-input");
editConversationTitleInputEl.value = conversationTitle;
editConversationTitleInputEl.addEventListener('click', function(event) {
event.stopPropagation();
});
editConversationTitleInputEl.addEventListener('keydown', function(event) {
if (event.key === "Enter") {
event.preventDefault();
editConversationTitleSaveButtonEl.click();
}
});
let editConversationTitleSaveButtonEl = this.contentEl.createEl('button');
editConversationTitleInputBoxEl.appendChild(editConversationTitleInputEl);
editConversationTitleSaveButtonEl.innerHTML = "Save";
editConversationTitleSaveButtonEl.classList.add("three-dot-menu-button-item");
editConversationTitleSaveButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
let newTitle = editConversationTitleInputEl.value;
if (newTitle != null) {
let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`;
fetch(`${this.setting.khojUrl}${editURL}` , { method: "PATCH", headers })
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(data => {
conversationSessionEl.textContent = newTitle;
})
.catch(err => {
return;
});
editConversationTitleInputBoxEl.remove();
}});
editConversationTitleInputBoxEl.appendChild(editConversationTitleSaveButtonEl);
conversationMenuEl.appendChild(editConversationTitleInputBoxEl);
});
conversationMenuEl.appendChild(editConversationTitleButtonEl);
threeDotMenuEl.appendChild(conversationMenuEl);
let deleteConversationButtonEl = this.contentEl.createEl('button');
deleteConversationButtonEl.innerHTML = "Delete";
deleteConversationButtonEl.classList.add("delete-conversation-button");
deleteConversationButtonEl.classList.add("three-dot-menu-button-item");
deleteConversationButtonEl.addEventListener('click', () => {
// Ask for confirmation before deleting chat session
let confirmation = confirm('Are you sure you want to delete this chat session?');
if (!confirmation) return;
let deleteURL = `/api/chat/history?client=obsidian&conversation_id=${incomingConversationId}`;
fetch(`${this.setting.khojUrl}${deleteURL}` , { method: "DELETE", headers })
.then(response => response.ok ? response.json() : Promise.reject(response))
.then(data => {
chatBodyEl.innerHTML = "";
chatBodyEl.dataset.conversationId = "";
chatBodyEl.dataset.conversationTitle = "";
this.getChatHistory(chatBodyEl);
})
.catch(err => {
return;
});
});
conversationMenuEl.appendChild(deleteConversationButtonEl);
threeDotMenuEl.appendChild(conversationMenuEl);
});
threeDotMenuEl.appendChild(threeDotMenuButton);
conversationSessionEl.appendChild(threeDotMenuEl);
conversationListBodyEl.appendChild(conversationSessionEl);
chatBodyEl.appendChild(sidePanelEl);
}
}
} catch (err) {
return false;
}
return true;
}
async getChatHistory(chatBodyEl: HTMLElement): Promise<boolean> { async getChatHistory(chatBodyEl: HTMLElement): Promise<boolean> {
// Get chat history from Khoj backend // Get chat history from Khoj backend
let chatUrl = `${this.setting.khojUrl}/api/chat/history?client=obsidian`; let chatUrl = `${this.setting.khojUrl}/api/chat/history?client=obsidian`;

View file

@ -239,6 +239,87 @@ img {
max-width: 60%; max-width: 60%;
} }
div.conversation-session {
color: var(--color-base-90);
border: 1px solid var(--khoj-storm-grey);
border-radius: 5px;
padding: 5px;
font-size: 14px;
font-weight: 300;
line-height: 1.5em;
cursor: pointer;
transition: background 0.2s ease-in-out;
text-align: left;
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr auto;
}
.three-dot-menu {
display: block;
/* background: var(--background-color); */
/* border: 1px solid var(--main-text-color); */
border-radius: 5px;
/* position: relative; */
}
button.three-dot-menu-button-item {
background: var(--khoj-winter-sun);
color: var(--khoj-storm-grey);
border: none;
box-shadow: none;
font-size: 14px;
font-weight: 300;
line-height: 1.5em;
cursor: pointer;
transition: background 0.3s ease-in-out;
font-family: var(--font-family);
border-radius: 4px;
right: 0;
}
button.three-dot-menu-button-item:hover {
background: var(--khoj-storm-grey);
color: var(--khoj-winter-sun);
}
.three-dot-menu-button {
background: var(--khoj-winter-sun);
border: none;
box-shadow: none;
font-size: 14px;
font-weight: 300;
line-height: 1.5em;
cursor: pointer;
transition: background 0.3s ease-in-out;
font-family: var(--font-family);
border-radius: 4px;
right: 0;
}
.conversation-button:hover .three-dot-menu {
display: block;
}
div.conversation-menu {
z-index: 1;
top: 100%;
right: 0;
text-align: right;
background-color: var(--khoj-winter-sun);
border: 1px solid var(--khoj-storm-grey);
border-radius: 5px;
padding: 5px;
box-shadow: 0 0 11px #aaa;
}
div.conversation-session:hover {
transform: scale(1.03);
}
div.selected-conversation {
background: var(--khoj-winter-sun) !important;
color: var(--khoj-storm-grey) !important;
}
#khoj-chat-footer { #khoj-chat-footer {
padding: 0; padding: 0;
display: grid; display: grid;