mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-30 10:53:02 +01:00
View, switch chat sessions from Obsidian chat pane
This commit is contained in:
parent
e86899eec4
commit
1ea7675fc9
2 changed files with 250 additions and 6 deletions
|
@ -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`;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue