From 870af19ba4c14a38977905d3d1e3569159998b16 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 17 Jan 2024 11:48:18 +0530 Subject: [PATCH 1/4] Update health API to pass authenticated users their info This allows Khoj clients to get email address associated with user's API token for display in client UX In anonymous mode, default user information is passed --- src/khoj/routers/api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index a6748d4f..01d939ae 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -554,6 +554,8 @@ async def extract_references_and_questions( return compiled_references, inferred_queries, defiltered_query -@api.get("/health") -async def health_check(): - return Response(status_code=200) +@api.get("/health", response_class=Response) +@requires(["authenticated"], status_code=200) +def health_check(request: Request) -> Response: + response_obj = {"email": request.user.object.email} + return Response(content=json.dumps(response_obj), media_type="application/json", status_code=200) From aab75a6eadaca74f99457b5100ac8ce0ed2ba6e0 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 17 Jan 2024 13:12:14 +0530 Subject: [PATCH 2/4] Improve Khoj server status check in Khoj Obsidian client - Update server connection status on every edit of khoj url, api key in settings instead of only on plugin load The error message was stale if connection fixed after changes in Khoj plugin settings to URL or API key, like on plugin install - Show better welcome message on first plugin install. Include API key setup instruction - Show logged in user email on Khoj settings page --- src/interface/obsidian/src/main.ts | 23 +++--------- src/interface/obsidian/src/settings.ts | 36 +++++++++++++------ src/interface/obsidian/src/utils.ts | 49 +++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index b45a7fb5..3e602340 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -1,8 +1,8 @@ -import { Notice, Plugin, request } from 'obsidian'; +import { Plugin } from 'obsidian'; import { KhojSetting, KhojSettingTab, DEFAULT_SETTINGS } from 'src/settings' import { KhojSearchModal } from 'src/search_modal' import { KhojChatModal } from 'src/chat_modal' -import { updateContentIndex } from './utils'; +import { updateContentIndex, canConnectToBackend } from './utils'; export default class Khoj extends Plugin { @@ -70,22 +70,9 @@ export default class Khoj extends Plugin { // Load khoj obsidian plugin settings this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - // Check if khoj backend is configured, note if cannot connect to backend - let headers = { "Authorization": `Bearer ${this.settings.khojApiKey}` }; - - if (this.settings.khojApiKey === "" && this.settings.khojUrl === "https://app.khoj.dev") { - new Notice(`❗️Khoj API key is not configured. Please visit https://app.khoj.dev/config#clients to get an API key.`); - return; - } - - await request({ url: this.settings.khojUrl ,method: "GET", headers: headers }) - .then(response => { - this.settings.connectedToBackend = true; - }) - .catch(error => { - this.settings.connectedToBackend = false; - new Notice(`❗️Ensure Khoj backend is running and Khoj URL is pointing to it in the plugin settings.\n\n${error}`); - }); + // Check if can connect to khoj server + ({ connectedToBackend: this.settings.connectedToBackend } = + await canConnectToBackend(this.settings.khojUrl, this.settings.khojApiKey, true)); } async saveSettings() { diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index 198251ad..4eb6553e 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -1,6 +1,6 @@ import { App, Notice, PluginSettingTab, Setting, TFile } from 'obsidian'; import Khoj from 'src/main'; -import { updateContentIndex } from './utils'; +import { canConnectToBackend, getBackendStatusMessage, updateContentIndex } from './utils'; export interface KhojSetting { resultsCount: number; @@ -9,6 +9,7 @@ export interface KhojSetting { connectedToBackend: boolean; autoConfigure: boolean; lastSyncedFiles: TFile[]; + userEmail: string; } export const DEFAULT_SETTINGS: KhojSetting = { @@ -17,7 +18,8 @@ export const DEFAULT_SETTINGS: KhojSetting = { khojApiKey: '', connectedToBackend: false, autoConfigure: true, - lastSyncedFiles: [] + lastSyncedFiles: [], + userEmail: '', } export class KhojSettingTab extends PluginSettingTab { @@ -33,7 +35,15 @@ export class KhojSettingTab extends PluginSettingTab { containerEl.empty(); // Add notice whether able to connect to khoj backend or not - containerEl.createEl('small', { text: this.getBackendStatusMessage() }); + let backendStatusEl = containerEl.createEl('small', { + text: getBackendStatusMessage( + this.plugin.settings.connectedToBackend, + this.plugin.settings.userEmail, + this.plugin.settings.khojUrl, + this.plugin.settings.khojApiKey + )} + ); + let backendStatusMessage: string = ''; // Add khoj settings configurable from the plugin settings tab new Setting(containerEl) @@ -43,8 +53,14 @@ export class KhojSettingTab extends PluginSettingTab { .setValue(`${this.plugin.settings.khojUrl}`) .onChange(async (value) => { this.plugin.settings.khojUrl = value.trim().replace(/\/$/, ''); + ({ + connectedToBackend: this.plugin.settings.connectedToBackend, + userEmail: this.plugin.settings.userEmail, + statusMessage: backendStatusMessage, + } = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey)); + await this.plugin.saveSettings(); - containerEl.firstElementChild?.setText(this.getBackendStatusMessage()); + backendStatusEl.setText(backendStatusMessage); })); new Setting(containerEl) .setName('Khoj API Key') @@ -53,7 +69,13 @@ export class KhojSettingTab extends PluginSettingTab { .setValue(`${this.plugin.settings.khojApiKey}`) .onChange(async (value) => { this.plugin.settings.khojApiKey = value.trim(); + ({ + connectedToBackend: this.plugin.settings.connectedToBackend, + userEmail: this.plugin.settings.userEmail, + statusMessage: backendStatusMessage, + } = await canConnectToBackend(this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey)); await this.plugin.saveSettings(); + backendStatusEl.setText(backendStatusMessage); })); new Setting(containerEl) .setName('Results Count') @@ -123,10 +145,4 @@ 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.'; - } } diff --git a/src/interface/obsidian/src/utils.ts b/src/interface/obsidian/src/utils.ts index 1f309ff6..829a0604 100644 --- a/src/interface/obsidian/src/utils.ts +++ b/src/interface/obsidian/src/utils.ts @@ -1,4 +1,4 @@ -import { FileSystemAdapter, Notice, Vault, Modal, TFile } from 'obsidian'; +import { FileSystemAdapter, Notice, Vault, Modal, TFile, request } from 'obsidian'; import { KhojSetting } from 'src/settings' export function getVaultAbsolutePath(vault: Vault): string { @@ -123,3 +123,50 @@ export async function createNoteAndCloseModal(query: string, modal: Modal, opt?: } modal.close(); } + +export async function canConnectToBackend( + khojUrl: string, + khojApiKey: string, + showNotice: boolean = false +): Promise<{ connectedToBackend: boolean; statusMessage: string, userEmail: string }> { + let connectedToBackend = false; + let userEmail: string = ''; + + if (!!khojUrl) { + let headers = !!khojApiKey ? { "Authorization": `Bearer ${khojApiKey}` } : undefined; + await request({ url: `${khojUrl}/api/health`, method: "GET", headers: headers }) + .then(response => { + connectedToBackend = true; + userEmail = JSON.parse(response)?.email; + }) + .catch(error => { + connectedToBackend = false; + console.log(`Khoj connection error:\n\n${error}`); + }); + } + + let statusMessage: string = getBackendStatusMessage(connectedToBackend, userEmail, khojUrl, khojApiKey); + if (showNotice) new Notice(statusMessage); + return { connectedToBackend, statusMessage, userEmail }; +} + +export function getBackendStatusMessage( + connectedToServer: boolean, + userEmail: string, + khojUrl: string, + khojApiKey: string +): string { + // Welcome message with default settings. Khoj cloud always expects an API key. + if (!!khojApiKey && khojUrl === 'https://app.khoj.dev') + return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`; + + if (!connectedToServer) + return `❗️Could not connect to Khoj at ${khojUrl}. Ensure your can access it`; + else if (!userEmail) + return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/config#clients to log in`; + else if (userEmail === 'default@example.com') + // Logged in as default user in anonymous mode + return `✅ Signed in to Khoj`; + else + return `✅ Signed in to Khoj as ${userEmail}`; +} From 36bf42a860d46813b694ad3fa187a8bb9d3863ec Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 17 Jan 2024 13:21:10 +0530 Subject: [PATCH 3/4] Show Khoj chat by default in Obsidian side pane instead of search --- src/interface/obsidian/src/main.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index 3e602340..c3accc3d 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -46,11 +46,8 @@ export default class Khoj extends Plugin { }); // Create an icon in the left ribbon. - this.addRibbonIcon('search', 'Khoj', (_: MouseEvent) => { - // Called when the user clicks the icon. - this.settings.connectedToBackend - ? 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`); + this.addRibbonIcon('message-circle', 'Khoj', (_: MouseEvent) => { + new KhojChatModal(this.app, this.settings).open() }); // Add a settings tab so the user can configure khoj From f9420e12090160ebb8f227dd6294c88caaa7cea8 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 17 Jan 2024 13:29:17 +0530 Subject: [PATCH 4/4] Show Khoj Obsidian commands even if no connection to server Server connection check can be a little flaky in Obsidian. Don't gate the commands behind it to improve usability of Khoj. Previously the commands would get disabled when server connection check failed, even though server was actually accessible --- src/interface/obsidian/src/main.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index c3accc3d..6b24ee37 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -16,33 +16,21 @@ export default class Khoj extends Plugin { this.addCommand({ id: 'search', name: 'Search', - checkCallback: (checking) => { - if (!checking && this.settings.connectedToBackend) - new KhojSearchModal(this.app, this.settings).open(); - return this.settings.connectedToBackend; - } + callback: () => { new KhojSearchModal(this.app, this.settings).open(); } }); // Add similar notes command. It can only be triggered from the editor this.addCommand({ id: 'similar', name: 'Find similar notes', - editorCheckCallback: (checking) => { - if (!checking && this.settings.connectedToBackend) - new KhojSearchModal(this.app, this.settings, true).open(); - return this.settings.connectedToBackend; - } + editorCallback: () => { new KhojSearchModal(this.app, this.settings, true).open(); } }); // 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; - } + callback: () => { new KhojChatModal(this.app, this.settings).open(); } }); // Create an icon in the left ribbon.