From c0972e09e64ea1d17663d28629bb6b55fd44c9c6 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 23 Mar 2023 05:30:11 +0400 Subject: [PATCH 01/11] Rename KhojModal to KhojSearchModal, a more specific name for it In preparation to introduce Khoj chat in Obsidian --- src/interface/obsidian/src/main.ts | 10 +++++----- .../obsidian/src/{modal.ts => search_modal.ts} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/interface/obsidian/src/{modal.ts => search_modal.ts} (98%) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index b367037c..5feb3364 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -1,6 +1,6 @@ import { Notice, Plugin } from 'obsidian'; import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings' -import { KhojModal } from 'src/modal' +import { KhojSearchModal } from 'src/search_modal' import { configureKhojBackend } from './utils'; @@ -16,7 +16,7 @@ export default class Khoj extends Plugin { name: 'Search', checkCallback: (checking) => { if (!checking && this.settings.connectedToBackend) - new KhojModal(this.app, this.settings).open(); + new KhojSearchModal(this.app, this.settings).open(); return this.settings.connectedToBackend; } }); @@ -27,7 +27,7 @@ export default class Khoj extends Plugin { name: 'Find similar notes', editorCheckCallback: (checking) => { if (!checking && this.settings.connectedToBackend) - new KhojModal(this.app, this.settings, true).open(); + new KhojSearchModal(this.app, this.settings, true).open(); return this.settings.connectedToBackend; } }); @@ -36,7 +36,7 @@ export default class Khoj extends Plugin { this.addRibbonIcon('search', 'Khoj', (_: MouseEvent) => { // Called when the user clicks the icon. this.settings.connectedToBackend - ? new KhojModal(this.app, this.settings).open() + ? new KhojSearchModal(this.app, this.settings).open() : new Notice(`❗️Ensure Khoj backend is running and Khoj URL is pointing to it in the plugin settings`); }); @@ -59,5 +59,5 @@ export default class Khoj extends Plugin { await configureKhojBackend(this.app.vault, this.settings, false); } this.saveData(this.settings); - } + } } diff --git a/src/interface/obsidian/src/modal.ts b/src/interface/obsidian/src/search_modal.ts similarity index 98% rename from src/interface/obsidian/src/modal.ts rename to src/interface/obsidian/src/search_modal.ts index bf9ad7b7..db06caaa 100644 --- a/src/interface/obsidian/src/modal.ts +++ b/src/interface/obsidian/src/search_modal.ts @@ -6,7 +6,7 @@ export interface SearchResult { file: string; } -export class KhojModal extends SuggestModal { +export class KhojSearchModal extends SuggestModal { setting: KhojSetting; rerank: boolean = false; find_similar_notes: boolean; From cd46a17e5f59cf8b490ddfe4a9f38dfe5c36d8e9 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 23 Mar 2023 05:31:26 +0400 Subject: [PATCH 02/11] Add Khoj Chat Modal, Command in Khoj Obsidian to Chat using API --- src/interface/obsidian/src/chat_modal.ts | 63 ++++++++++++++++++++++++ src/interface/obsidian/src/main.ts | 12 +++++ 2 files changed, 75 insertions(+) create mode 100644 src/interface/obsidian/src/chat_modal.ts diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts new file mode 100644 index 00000000..ca2248a5 --- /dev/null +++ b/src/interface/obsidian/src/chat_modal.ts @@ -0,0 +1,63 @@ +import { App, Modal, request, Setting } from 'obsidian'; +import { KhojSetting } from 'src/settings'; + + +export class KhojChatModal extends Modal { + result: string; + setting: KhojSetting; + + constructor(app: App, setting: KhojSetting) { + super(app); + this.setting = setting; + } + + async onOpen() { + let { contentEl } = this; + + // Add title to the Khoj Chat modal + contentEl.createEl("h1", { text: "Khoj Chat" }); + + // Create area for chat logs + contentEl.createDiv({ attr: { class: "chat-body" } }); + + // Add chat input field + new Setting(contentEl) + .addText((text) => { + text.onChange((value) => { this.result = value }); + text.setPlaceholder("What is the meaning of life?"); + }) + .addButton((btn) => btn + .setButtonText("Send") + .setCta() + .onClick(async () => { await this.getChatResponse(this.result) })); + } + + renderMessage(message: string, sender: string) { + let { contentEl } = this; + + // Append message to conversation history HTML element. + // The chat logs should display above the message input box to follow standard UI semantics + let chat_body_el = contentEl.getElementsByClassName("chat-body").item(0); + if (!!chat_body_el) { + let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You"; + chat_body_el.createDiv({ text: `${emojified_sender}: ${message}` }) + } + } + + async getChatResponse(query: string): Promise { + // Exit if query is empty + if (!query || query === "") return; + + // Render user query as chat message + this.renderMessage(query, "you"); + + // Get chat response from Khoj backend + let encodedQuery = encodeURIComponent(query); + let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}`; + let response = await request(chatUrl); + let data = JSON.parse(response); + + // Render Khoj response as chat message + this.renderMessage(data.response, "khoj"); + } +} diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index 5feb3364..470e1e89 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -1,6 +1,7 @@ import { Notice, Plugin } from 'obsidian'; import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings' import { KhojSearchModal } from 'src/search_modal' +import { KhojChatModal } from 'src/chat_modal' import { configureKhojBackend } from './utils'; @@ -32,6 +33,17 @@ export default class Khoj extends Plugin { } }); + // Add chat command. It can be triggered from anywhere + this.addCommand({ + id: 'chat', + name: 'Chat', + checkCallback: (checking) => { + if (!checking && this.settings.connectedToBackend) + new KhojChatModal(this.app, this.settings).open(); + return this.settings.connectedToBackend; + } + }); + // Create an icon in the left ribbon. this.addRibbonIcon('search', 'Khoj', (_: MouseEvent) => { // Called when the user clicks the icon. From 1d3d949962783393f152cc80e2fe365271ea206d Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 23 Mar 2023 08:16:54 +0400 Subject: [PATCH 03/11] Render conversation logs on page load --- src/interface/obsidian/src/chat_modal.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index ca2248a5..c93fe7e6 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -20,6 +20,14 @@ export class KhojChatModal extends Modal { // Create area for chat logs contentEl.createDiv({ attr: { class: "chat-body" } }); + // Get conversation history from Khoj backend + let chatUrl = `${this.setting.khojUrl}/api/chat?`; + let response = await request(chatUrl); + let chatLogs = JSON.parse(response).response; + chatLogs.forEach( (chatLog: any) => { + this.renderMessage(chatLog.message, chatLog.by); + }); + // Add chat input field new Setting(contentEl) .addText((text) => { From 112f388ada398840c3e91a000cce16f639b58287 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 28 Mar 2023 15:35:06 +0700 Subject: [PATCH 04/11] Render references next to chat responses by khoj in chat modal --- src/interface/obsidian/src/chat_modal.ts | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index c93fe7e6..99e1c421 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -24,8 +24,8 @@ export class KhojChatModal extends Modal { let chatUrl = `${this.setting.khojUrl}/api/chat?`; let response = await request(chatUrl); let chatLogs = JSON.parse(response).response; - chatLogs.forEach( (chatLog: any) => { - this.renderMessage(chatLog.message, chatLog.by); + chatLogs.forEach((chatLog: any) => { + this.renderMessageWithReferences(chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created)); }); // Add chat input field @@ -40,7 +40,26 @@ export class KhojChatModal extends Modal { .onClick(async () => { await this.getChatResponse(this.result) })); } - renderMessage(message: string, sender: string) { + generateReference(messageEl: any, reference: string, index: number) { + // Generate HTML for Chat Reference + // `${index}`; + let escaped_ref = reference.replace(/"/g, "\\\"") + return messageEl.createEl("sup").createEl("abbr", { + attr: { + title: escaped_ref, + tabindex: "0", + }, + text: `[${index}] `, + }); + } + + renderMessageWithReferences(message: string, sender: string, context?: [string], dt?: Date) { + let messageEl = this.renderMessage(message, sender, dt); + if (context && !!messageEl) { + context.map((reference, index) => this.generateReference(messageEl, reference, index)); + } + } + renderMessage(message: string, sender: string, dt?: Date): Element | null { let { contentEl } = this; // Append message to conversation history HTML element. @@ -50,6 +69,8 @@ export class KhojChatModal extends Modal { let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You"; chat_body_el.createDiv({ text: `${emojified_sender}: ${message}` }) } + + return chat_body_el } async getChatResponse(query: string): Promise { From 001ac7b5eb5c8352c10082294f88113c1a7b3812 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 28 Mar 2023 18:04:27 +0700 Subject: [PATCH 05/11] Style Obsidian Chat Modal like Khoj Chat Web Interface - Add message sender, date metadata as message footer - Use css directly from Khoj Chat Web Interface. - Modify it to work under a Obsidian modal - So replace html, body styling from web interface to instead styling new "khoj-chat" class attached to contentEl of modal --- src/interface/obsidian/src/chat_modal.ts | 37 ++++-- src/interface/obsidian/styles.css | 141 +++++++++++++++++++++++ 2 files changed, 168 insertions(+), 10 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 99e1c421..3b476dc3 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -13,12 +13,13 @@ export class KhojChatModal extends Modal { async onOpen() { let { contentEl } = this; + contentEl.addClass("khoj-chat"); // Add title to the Khoj Chat modal - contentEl.createEl("h1", { text: "Khoj Chat" }); + contentEl.createEl("h1", ({ attr: { id: "chat-title" }, text: "Khoj Chat" })); // Create area for chat logs - contentEl.createDiv({ attr: { class: "chat-body" } }); + contentEl.createDiv({ attr: { id: "chat-body", class: "chat-body" } }); // Get conversation history from Khoj backend let chatUrl = `${this.setting.khojUrl}/api/chat?`; @@ -59,21 +60,37 @@ export class KhojChatModal extends Modal { context.map((reference, index) => this.generateReference(messageEl, reference, index)); } } + renderMessage(message: string, sender: string, dt?: Date): Element | null { - let { contentEl } = this; + let message_time = this.formatDate(dt ?? new Date()); + let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You"; // Append message to conversation history HTML element. // The chat logs should display above the message input box to follow standard UI semantics - let chat_body_el = contentEl.getElementsByClassName("chat-body").item(0); - if (!!chat_body_el) { - let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You"; - chat_body_el.createDiv({ text: `${emojified_sender}: ${message}` }) - } + let chat_body_el = this.contentEl.getElementsByClassName("chat-body")[0]; + let chat_message_el = chat_body_el.createDiv({ + attr: { + "data-meta": `${emojified_sender} at ${message_time}`, + class: `chat-message ${sender}` + }, + }).createDiv({ + attr: { + class: `chat-message-text ${sender}` + }, + text: `${message}` + }) - return chat_body_el + return chat_message_el } - async getChatResponse(query: string): Promise { + formatDate(date: Date): string { + // Format date in HH:MM, DD MMM YYYY format + let time_string = date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false }); + let date_string = date.toLocaleString('en-IN', { year: 'numeric', month: 'short', day: '2-digit' }).replace(/-/g, ' '); + return `${time_string}, ${date_string}`; + } + + async getChatResponse(query: string | undefined | null): Promise { // Exit if query is empty if (!query || query === "") return; diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 71cc60fd..99703d3e 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -6,3 +6,144 @@ available in the app when your plugin is enabled. If your plugin does not need CSS, delete this file. */ + +:root { + --khoj-chat-blue: #017eff; + --khoj-chat-dark-grey: #475569; + --khoj-chat-light-grey: #aaa; + --khoj-chat-white: #f8fafc; +} + +.khoj-chat { + display: grid; + color: var(--khoj-chat-dark-grey); + text-align: center; + font-family: roboto, karma, segoe ui, sans-serif; + font-size: 20px; + font-weight: 300; + line-height: 1.5em; +} +.khoj-chat > * { + padding: 10px; + margin: 10px; +} + +#chat-title { + font-weight: 200; + color: var(--khoj-chat-blue); +} + +#chat-body { + font-size: medium; + margin: 0px; + line-height: 20px; + overflow-y: scroll; /* Make chat body scroll to see history */ +} +/* add chat metatdata to bottom of bubble */ +.chat-message::after { + content: attr(data-meta); + display: block; + font-size: x-small; + color: var(--khoj-chat-dark-grey); + margin: -12px 7px 0 -5px; +} +/* move message by khoj to left */ +.chat-message.khoj { + margin-left: auto; + text-align: left; +} +/* move message by you to right */ +.chat-message.you { + margin-right: auto; + text-align: right; +} +/* basic style chat message text */ +.chat-message-text { + margin: 10px; + border-radius: 10px; + padding: 10px; + position: relative; + display: inline-block; + max-width: 80%; + text-align: left; +} +/* color chat bubble by khoj blue */ +.chat-message-text.khoj { + color: var(--khoj-chat-white); + background: var(--khoj-chat-blue); + margin-left: auto; + white-space: pre-line; +} +/* add left protrusion to khoj chat bubble */ +.chat-message-text.khoj:after { + content: ''; + position: absolute; + bottom: -2px; + left: -7px; + border: 10px solid transparent; + border-top-color: var(--khoj-chat-blue); + border-bottom: 0; + transform: rotate(-60deg); +} +/* color chat bubble by you dark grey */ +.chat-message-text.you { + color: var(--khoj-chat-white); + background: var(--khoj-chat-dark-grey); + margin-right: auto; +} +/* add right protrusion to you chat bubble */ +.chat-message-text.you:after { + content: ''; + position: absolute; + top: 91%; + right: -2px; + border: 10px solid transparent; + border-left-color: var(--khoj-chat-dark-grey); + border-right: 0; + margin-top: -10px; + transform: rotate(-60deg) +} + +#chat-footer { + padding: 0; + 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 var(--khoj-chat-dark-grey); + background: #f9fafc +} +#chat-input.option:hover { + box-shadow: 0 0 11px var(--khoj-chat-light-grey); +} +#chat-input { + font-size: medium; +} + +@media (pointer: coarse), (hover: none) { + #chat-body.abbr[title] { + position: relative; + padding-left: 4px; /* space references out to ease tapping */ + } + #chat-body.abbr[title]:focus:after { + content: attr(title); + + /* position tooltip */ + position: absolute; + left: 16px; /* open tooltip to right of ref link, instead of on top of it */ + width: auto; + z-index: 1; /* show tooltip above chat messages */ + + /* style tooltip */ + background-color: #aaa; + color: var(--khoj-chat-white); + border-radius: 2px; + box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.4); + font-size: 14px; + padding: 2px 4px; + } +} From 59ff1ae27f13e172030460efbcf73ff3ccdfe80c Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 28 Mar 2023 18:49:28 +0700 Subject: [PATCH 06/11] Use obsidian theme colors for bg, text. Restrict css namespace via prefix --- src/interface/obsidian/src/chat_modal.ts | 11 ++--- src/interface/obsidian/styles.css | 52 ++++++++++++------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 3b476dc3..59da36ac 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -16,10 +16,10 @@ export class KhojChatModal extends Modal { contentEl.addClass("khoj-chat"); // Add title to the Khoj Chat modal - contentEl.createEl("h1", ({ attr: { id: "chat-title" }, text: "Khoj Chat" })); + contentEl.createEl("h1", ({ attr: { id: "khoj-chat-title" }, text: "Khoj Chat" })); // Create area for chat logs - contentEl.createDiv({ attr: { id: "chat-body", class: "chat-body" } }); + contentEl.createDiv({ attr: { id: "khoj-chat-body", class: "khoj-chat-body" } }); // Get conversation history from Khoj backend let chatUrl = `${this.setting.khojUrl}/api/chat?`; @@ -37,6 +37,7 @@ export class KhojChatModal extends Modal { }) .addButton((btn) => btn .setButtonText("Send") + .setClass("khoj-chat-input-button") .setCta() .onClick(async () => { await this.getChatResponse(this.result) })); } @@ -67,15 +68,15 @@ export class KhojChatModal extends Modal { // Append message to conversation history HTML element. // The chat logs should display above the message input box to follow standard UI semantics - let chat_body_el = this.contentEl.getElementsByClassName("chat-body")[0]; + let chat_body_el = this.contentEl.getElementsByClassName("khoj-chat-body")[0]; let chat_message_el = chat_body_el.createDiv({ attr: { "data-meta": `${emojified_sender} at ${message_time}`, - class: `chat-message ${sender}` + class: `khoj-chat-message ${sender}` }, }).createDiv({ attr: { - class: `chat-message-text ${sender}` + class: `khoj-chat-message-text ${sender}` }, text: `${message}` }) diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 99703d3e..a1ee31dd 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -10,13 +10,12 @@ If your plugin does not need CSS, delete this file. :root { --khoj-chat-blue: #017eff; --khoj-chat-dark-grey: #475569; - --khoj-chat-light-grey: #aaa; - --khoj-chat-white: #f8fafc; } .khoj-chat { display: grid; - color: var(--khoj-chat-dark-grey); + background: var(--background-primary); + color: var(--text-normal); text-align: center; font-family: roboto, karma, segoe ui, sans-serif; font-size: 20px; @@ -28,37 +27,37 @@ If your plugin does not need CSS, delete this file. margin: 10px; } -#chat-title { +#khoj-chat-title { font-weight: 200; color: var(--khoj-chat-blue); } -#chat-body { +#khoj-chat-body { font-size: medium; margin: 0px; line-height: 20px; overflow-y: scroll; /* Make chat body scroll to see history */ } /* add chat metatdata to bottom of bubble */ -.chat-message::after { +.khoj-chat-message::after { content: attr(data-meta); display: block; font-size: x-small; - color: var(--khoj-chat-dark-grey); + color: var(--text-muted); margin: -12px 7px 0 -5px; } /* move message by khoj to left */ -.chat-message.khoj { +.khoj-chat-message.khoj { margin-left: auto; text-align: left; } /* move message by you to right */ -.chat-message.you { +.khoj-chat-message.you { margin-right: auto; text-align: right; } /* basic style chat message text */ -.chat-message-text { +.khoj-chat-message-text { margin: 10px; border-radius: 10px; padding: 10px; @@ -68,14 +67,14 @@ If your plugin does not need CSS, delete this file. text-align: left; } /* color chat bubble by khoj blue */ -.chat-message-text.khoj { - color: var(--khoj-chat-white); +.khoj-chat-message-text.khoj { + color: var(--text-on-accent); background: var(--khoj-chat-blue); margin-left: auto; white-space: pre-line; } /* add left protrusion to khoj chat bubble */ -.chat-message-text.khoj:after { +.khoj-chat-message-text.khoj:after { content: ''; position: absolute; bottom: -2px; @@ -86,13 +85,13 @@ If your plugin does not need CSS, delete this file. transform: rotate(-60deg); } /* color chat bubble by you dark grey */ -.chat-message-text.you { - color: var(--khoj-chat-white); +.khoj-chat-message-text.you { + color: var(--text-on-accent); background: var(--khoj-chat-dark-grey); margin-right: auto; } /* add right protrusion to you chat bubble */ -.chat-message-text.you:after { +.khoj-chat-message-text.you:after { content: ''; position: absolute; top: 91%; @@ -104,32 +103,33 @@ If your plugin does not need CSS, delete this file. transform: rotate(-60deg) } -#chat-footer { +#khoj-chat-footer { padding: 0; display: grid; grid-template-columns: minmax(70px, 100%); grid-column-gap: 10px; grid-row-gap: 10px; } -#chat-footer > * { +#khoj-chat-footer > * { padding: 15px; border-radius: 5px; border: 1px solid var(--khoj-chat-dark-grey); background: #f9fafc } -#chat-input.option:hover { - box-shadow: 0 0 11px var(--khoj-chat-light-grey); +#khoj-chat-input.option:hover { + box-shadow: 0 0 11px var(--background-modifier-box-shadow); } -#chat-input { +#khoj-chat-input { font-size: medium; } +} @media (pointer: coarse), (hover: none) { - #chat-body.abbr[title] { + #khoj-chat-body.abbr[title] { position: relative; padding-left: 4px; /* space references out to ease tapping */ } - #chat-body.abbr[title]:focus:after { + #khoj-chat-body.abbr[title]:focus:after { content: attr(title); /* position tooltip */ @@ -139,10 +139,10 @@ If your plugin does not need CSS, delete this file. z-index: 1; /* show tooltip above chat messages */ /* style tooltip */ - background-color: #aaa; - color: var(--khoj-chat-white); + background-color: var(--background-secondary); + color: var(--text-muted); border-radius: 2px; - box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.4); + box-shadow: 1px 1px 4px 0 var(--background-modifier-box-shadow); font-size: 14px; padding: 2px 4px; } From 81e98c30797b2f6845b2d4ba27b3928f1a3876df Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 29 Mar 2023 12:41:33 +0700 Subject: [PATCH 07/11] Scroll to bottom of modal on open and message send --- src/interface/obsidian/src/chat_modal.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 59da36ac..b583702c 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -40,6 +40,9 @@ export class KhojChatModal extends Modal { .setClass("khoj-chat-input-button") .setCta() .onClick(async () => { await this.getChatResponse(this.result) })); + + // Scroll to bottom of modal, till the send message input box + this.modalEl.scrollTop = this.modalEl.scrollHeight; } generateReference(messageEl: any, reference: string, index: number) { @@ -81,6 +84,9 @@ export class KhojChatModal extends Modal { text: `${message}` }) + // Scroll to bottom after inserting chat messages + this.modalEl.scrollTop = this.modalEl.scrollHeight; + return chat_message_el } From 23bd737f6be5b8513cc91d2087f63f93d9725b0a Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 29 Mar 2023 17:47:04 +0700 Subject: [PATCH 08/11] Use chat input element to send message on Enter. No send button required --- src/interface/obsidian/src/chat_modal.ts | 32 +++++++++++++++++------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index b583702c..b1cbabf9 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -9,6 +9,19 @@ export class KhojChatModal extends Modal { constructor(app: App, setting: KhojSetting) { super(app); this.setting = setting; + + // Register Modal Keybindings to send user message + this.scope.register([], 'Enter', async () => { + // Get text in chat input elmenet + let input_el = this.contentEl.getElementsByClassName("khoj-chat-input")[0]; + + // Clear text after extracting message to send + let user_message = input_el.value; + input_el.value = ""; + + // Get and render chat response to user message + await this.getChatResponse(user_message); + }); } async onOpen() { @@ -30,16 +43,17 @@ export class KhojChatModal extends Modal { }); // Add chat input field - new Setting(contentEl) - .addText((text) => { - text.onChange((value) => { this.result = value }); - text.setPlaceholder("What is the meaning of life?"); + contentEl.createEl("input", + { + attr: { + type: "text", + id: "khoj-chat-input", + autofocus: "autofocus", + placeholder: "What is the meaning of life? [Hit Enter to send message]", + class: "khoj-chat-input option" + } }) - .addButton((btn) => btn - .setButtonText("Send") - .setClass("khoj-chat-input-button") - .setCta() - .onClick(async () => { await this.getChatResponse(this.result) })); + .addEventListener('change', (event) => { this.result = (event.target).value }); // Scroll to bottom of modal, till the send message input box this.modalEl.scrollTop = this.modalEl.scrollHeight; From 3d616c8d65179c382d2f5d6d77bc1aaa60cc9053 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 29 Mar 2023 22:12:11 +0700 Subject: [PATCH 09/11] Use Obsidian font sizes. Improve input field, reference indexing - Give space in the input field. Too narrow previously - References should be indexed from 1 instead of 0 - Use Obsidian font size variables to scale fonts in chat appropriately --- src/interface/obsidian/src/chat_modal.ts | 4 ++-- src/interface/obsidian/styles.css | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index b1cbabf9..ffbdb80d 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -49,7 +49,7 @@ export class KhojChatModal extends Modal { type: "text", id: "khoj-chat-input", autofocus: "autofocus", - placeholder: "What is the meaning of life? [Hit Enter to send message]", + placeholder: "Chat with Khoj 🦅 [Hit Enter to send message]", class: "khoj-chat-input option" } }) @@ -75,7 +75,7 @@ export class KhojChatModal extends Modal { renderMessageWithReferences(message: string, sender: string, context?: [string], dt?: Date) { let messageEl = this.renderMessage(message, sender, dt); if (context && !!messageEl) { - context.map((reference, index) => this.generateReference(messageEl, reference, index)); + context.map((reference, index) => this.generateReference(messageEl, reference, index+1)); } } diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index a1ee31dd..cb4e002f 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -18,7 +18,7 @@ If your plugin does not need CSS, delete this file. color: var(--text-normal); text-align: center; font-family: roboto, karma, segoe ui, sans-serif; - font-size: 20px; + font-size: var(--font-ui-large); font-weight: 300; line-height: 1.5em; } @@ -33,7 +33,7 @@ If your plugin does not need CSS, delete this file. } #khoj-chat-body { - font-size: medium; + font-size: var(--font-ui-medium); margin: 0px; line-height: 20px; overflow-y: scroll; /* Make chat body scroll to see history */ @@ -42,7 +42,7 @@ If your plugin does not need CSS, delete this file. .khoj-chat-message::after { content: attr(data-meta); display: block; - font-size: x-small; + font-size: var(--font-ui-smaller); color: var(--text-muted); margin: -12px 7px 0 -5px; } @@ -112,16 +112,14 @@ If your plugin does not need CSS, delete this file. } #khoj-chat-footer > * { padding: 15px; - border-radius: 5px; - border: 1px solid var(--khoj-chat-dark-grey); background: #f9fafc } #khoj-chat-input.option:hover { box-shadow: 0 0 11px var(--background-modifier-box-shadow); } #khoj-chat-input { - font-size: medium; -} + font-size: var(--font-ui-medium); + padding: 25px 20px; } @media (pointer: coarse), (hover: none) { @@ -143,7 +141,7 @@ If your plugin does not need CSS, delete this file. color: var(--text-muted); border-radius: 2px; box-shadow: 1px 1px 4px 0 var(--background-modifier-box-shadow); - font-size: 14px; + font-size: var(--font-ui-small); padding: 2px 4px; } } From 7ecae224e7a885b0316da83f77810d195665a522 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 29 Mar 2023 23:42:17 +0700 Subject: [PATCH 10/11] Configure OpenAI API Key from the Khoj plugin setting in Obsidian --- src/interface/obsidian/src/main.ts | 4 +- src/interface/obsidian/src/settings.ts | 17 +++++-- src/interface/obsidian/src/utils.ts | 69 ++++++++++++++++++-------- 3 files changed, 63 insertions(+), 27 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index 470e1e89..6d4af194 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -38,9 +38,9 @@ export default class Khoj extends Plugin { id: 'chat', name: 'Chat', checkCallback: (checking) => { - if (!checking && this.settings.connectedToBackend) + if (!checking && this.settings.connectedToBackend && !!this.settings.openaiApiKey) new KhojChatModal(this.app, this.settings).open(); - return this.settings.connectedToBackend; + return !!this.settings.openaiApiKey; } }); diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index d7f41ba7..2cdc79a5 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -2,6 +2,7 @@ import { App, Notice, PluginSettingTab, request, Setting } from 'obsidian'; import Khoj from 'src/main'; export interface KhojSetting { + openaiApiKey: string; resultsCount: number; khojUrl: string; connectedToBackend: boolean; @@ -13,6 +14,7 @@ export const DEFAULT_SETTINGS: KhojSetting = { khojUrl: 'http://localhost:8000', connectedToBackend: false, autoConfigure: true, + openaiApiKey: '', } export class KhojSettingTab extends PluginSettingTab { @@ -41,7 +43,16 @@ export class KhojSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); containerEl.firstElementChild?.setText(this.getBackendStatusMessage()); })); - new Setting(containerEl) + new Setting(containerEl) + .setName('OpenAI API Key') + .setDesc('Your OpenAI API Key for Khoj Chat') + .addText(text => text + .setValue(`${this.plugin.settings.openaiApiKey}`) + .onChange(async (value) => { + this.plugin.settings.openaiApiKey = value.trim(); + await this.plugin.saveSettings(); + })); + new Setting(containerEl) .setName('Results Count') .setDesc('The number of search results to show') .addSlider(slider => slider @@ -110,7 +121,7 @@ export class KhojSettingTab extends PluginSettingTab { getBackendStatusMessage() { return !this.plugin.settings.connectedToBackend - ? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.' - : '✅ Connected to Khoj backend.'; + ? '❗Disconnected from Khoj backend. Ensure Khoj backend is running and Khoj URL is correctly set below.' + : '✅ Connected to Khoj backend.'; } } diff --git a/src/interface/obsidian/src/utils.ts b/src/interface/obsidian/src/utils.ts index 04e1919c..ba539179 100644 --- a/src/interface/obsidian/src/utils.ts +++ b/src/interface/obsidian/src/utils.ts @@ -29,10 +29,11 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n // Set index name from the path of the current vault let indexName = getVaultAbsolutePath(vault).replace(/\//g, '_').replace(/ /g, '_'); - // Get default index directory from khoj backend - let khojDefaultIndexDirectory = await request(`${khojConfigUrl}/default`) - .then(response => JSON.parse(response)) - .then(data => { return getIndexDirectoryFromBackendConfig(data); }); + // Get default config fields from khoj backend + let defaultConfig = await request(`${khojConfigUrl}/default`).then(response => JSON.parse(response)); + let khojDefaultIndexDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["content-type"]["markdown"]["embeddings-file"]); + let khojDefaultChatDirectory = getIndexDirectoryFromBackendConfig(defaultConfig["processor"]["conversation"]["conversation-logfile"]); + let khojDefaultChatModelName = defaultConfig["processor"]["conversation"]["model"]; // Get current config if khoj backend configured, else get default config from khoj backend await request(khoj_already_configured ? khojConfigUrl : `${khojConfigUrl}/default`) @@ -49,14 +50,7 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n "compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`, } } - // Disable khoj processors, as not required - delete data["processor"]; - - // Save new config and refresh index on khoj backend - updateKhojBackend(setting.khojUrl, data); - console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`) } - // Else if khoj config has no markdown content config else if (!data["content-type"]["markdown"]) { // Add markdown config to khoj content-type config @@ -67,28 +61,59 @@ export async function configureKhojBackend(vault: Vault, setting: KhojSetting, n "embeddings-file": `${khojDefaultIndexDirectory}/${indexName}.pt`, "compressed-jsonl": `${khojDefaultIndexDirectory}/${indexName}.jsonl.gz`, } - - // Save updated config and refresh index on khoj backend - updateKhojBackend(setting.khojUrl, data); - console.log(`Khoj: Added markdown config to khoj backend config:\n${JSON.stringify(data["content-type"])}`) } - // Else if khoj is not configured to index markdown files in configured obsidian vault else if (data["content-type"]["markdown"]["input-filter"].length != 1 || data["content-type"]["markdown"]["input-filter"][0] !== mdInVault) { // Update markdown config in khoj content-type config // Set markdown config to only index markdown files in configured obsidian vault - let khojIndexDirectory = getIndexDirectoryFromBackendConfig(data); + let khojIndexDirectory = getIndexDirectoryFromBackendConfig(data["content-type"]["markdown"]["embeddings-file"]); data["content-type"]["markdown"] = { "input-filter": [mdInVault], "input-files": null, "embeddings-file": `${khojIndexDirectory}/${indexName}.pt`, "compressed-jsonl": `${khojIndexDirectory}/${indexName}.jsonl.gz`, } - // Save updated config and refresh index on khoj backend - updateKhojBackend(setting.khojUrl, data); - console.log(`Khoj: Updated markdown config in khoj backend config:\n${JSON.stringify(data["content-type"]["markdown"])}`) } + + // If OpenAI API key not set in Khoj plugin settings + if (!setting.openaiApiKey) { + // Disable khoj processors, as not required + delete data["processor"]; + } + // Else if khoj backend not configured yet + else if (!khoj_already_configured || !data["processor"]) { + data["processor"] = { + "conversation": { + "conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`, + "model": khojDefaultChatModelName, + "openai-api-key": setting.openaiApiKey, + } + } + } + // Else if khoj config has no conversation processor config + else if (!data["processor"]["conversation"]) { + data["processor"]["conversation"] = { + "conversation-logfile": `${khojDefaultChatDirectory}/conversation.json`, + "model": khojDefaultChatModelName, + "openai-api-key": setting.openaiApiKey, + } + } + // Else if khoj is not configured with OpenAI API key from khoj plugin settings + else if (data["processor"]["conversation"]["openai-api-key"] !== setting.openaiApiKey) { + data["processor"]["conversation"] = { + "conversation-logfile": data["processor"]["conversation"]["conversation-logfile"], + "model": data["procesor"]["conversation"]["model"], + "openai-api-key": setting.openaiApiKey, + } + } + + // Save updated config and refresh index on khoj backend + updateKhojBackend(setting.khojUrl, data); + if (!khoj_already_configured) + console.log(`Khoj: Created khoj backend config:\n${JSON.stringify(data)}`) + else + console.log(`Khoj: Updated khoj backend config:\n${JSON.stringify(data)}`) }) .catch(error => { if (notify) @@ -111,6 +136,6 @@ export async function updateKhojBackend(khojUrl: string, khojConfig: Object) { .then(_ => request(`${khojUrl}/api/update?t=markdown`)); } -function getIndexDirectoryFromBackendConfig(khojConfig: any) { - return khojConfig["content-type"]["markdown"]["embeddings-file"].split("/").slice(0, -1).join("/"); +function getIndexDirectoryFromBackendConfig(filepath: string) { + return filepath.split("/").slice(0, -1).join("/"); } From c8c0cfd10e7b7a0badd0271836ddeec19d2f7658 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 30 Mar 2023 00:32:24 +0700 Subject: [PATCH 11/11] Add Chat features, setup and usage to Khoj Obsidian plugin Readme --- src/interface/obsidian/README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/interface/obsidian/README.md b/src/interface/obsidian/README.md index b3195bb0..8c6c1421 100644 --- a/src/interface/obsidian/README.md +++ b/src/interface/obsidian/README.md @@ -12,6 +12,7 @@ - [Setup Plugin](#2-Setup-Plugin) - [Use](#Use) - [Search](#search) + - [Chat](#chat) - [Find Similar Notes](#find-similar-notes) - [Upgrade](#Upgrade) - [Upgrade Backend](#1-Upgrade-Backend) @@ -21,9 +22,14 @@ - [Implementation](#Implementation) ## Features -- **Natural**: Advanced natural language understanding using Transformer based ML Models -- **Local**: Your personal data stays local. All search, indexing is done on your machine[\*](https://github.com/debanjum/khoj#miscellaneous) -- **Incremental**: Incremental search for a fast, search-as-you-type experience +- **Search** + - **Natural**: Advanced natural language understanding using Transformer based ML Models + - **Local**: Your personal data stays local. All search and indexing is done on your machine. *Unlike chat which requires access to GPT.* + - **Incremental**: Incremental search for a fast, search-as-you-type experience +- **Chat** + - **Faster answers**: Find answers faster and with less effort than search + - **Iterative discovery**: Iteratively explore and (re-)discover your notes + - **Assisted creativity**: Smoothly weave across answers retrieval and content generation ## Demo https://user-images.githubusercontent.com/6413477/210486007-36ee3407-e6aa-4185-8a26-b0bfc0a4344f.mp4 @@ -55,10 +61,21 @@ pip install khoj-assistant && khoj --no-gui ### 2. Setup Plugin 1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel 2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian + 3. [Optional] To enable Khoj Chat, set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings See [official Obsidian plugin docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for details ## Use +### Chat +Run *Khoj: Chat* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette) and ask questions in a natural, conversational style. +E.g "When did I file my taxes last year?" + +Notes: +- *Using Khoj Chat will result in query relevant notes being shared with OpenAI for ChatGPT to respond.* +- *To use Khoj Chat, ensure you've set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings.* + +See [[https://github.com/debanjum/khoj/tree/master/#Khoj-Chat][Khoj Chat]] for more details + ### Search Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)