From 833ab52986566bf449537fb392426bcc0e0b7744 Mon Sep 17 00:00:00 2001 From: Henri Jamet <42291955+hjamet@users.noreply.github.com> Date: Sun, 29 Dec 2024 12:51:52 +0100 Subject: [PATCH 1/5] Add manual sync command to Khoj plugin - Introduced a new command 'Sync new changes' to allow users to manually synchronize new modifications. - The command updates the content index without regenerating it, ensuring only new changes are synced. - User-triggered notifications are displayed upon successful sync. --- src/interface/obsidian/src/main.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index bf7cad548..f5f80cfc4 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -34,6 +34,21 @@ export default class Khoj extends Plugin { callback: () => { this.activateView(KhojView.CHAT); } }); + // Add sync command to manually sync new changes + this.addCommand({ + id: 'sync', + name: 'Sync new changes', + callback: async () => { + this.settings.lastSync = await updateContentIndex( + this.app.vault, + this.settings, + this.settings.lastSync, + false, // regenerate = false pour ne synchroniser que les nouvelles modifications + true // userTriggered = true pour afficher une notification + ); + } + }); + this.registerView(KhojView.CHAT, (leaf) => new KhojChatView(leaf, this.settings)); // Create an icon in the left ribbon. From c5c9e0072c6a541fed9015c3d6d72b7216653a9e Mon Sep 17 00:00:00 2001 From: Henri Jamet <42291955+hjamet@users.noreply.github.com> Date: Sun, 29 Dec 2024 13:07:21 +0100 Subject: [PATCH 2/5] Enhance Khoj plugin settings and UI for folder synchronization - Added a new setting to manage sync folders, allowing users to specify which folders to sync or to sync the entire vault. - Implemented a modal for folder suggestions to facilitate folder selection. - Updated the folder list display to show currently selected folders with options to remove them. - Improved CSS styles for chat interface and folder list for better user experience. - Refactored code for consistency and readability across multiple files. --- src/interface/obsidian/src/settings.ts | 110 ++++++++++++++++- src/interface/obsidian/src/utils.ts | 100 ++++++++------- src/interface/obsidian/styles.css | 162 ++++++++++++++++++++++--- 3 files changed, 308 insertions(+), 64 deletions(-) diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index 6f70d609c..62467b88a 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -1,4 +1,4 @@ -import { App, Notice, PluginSettingTab, Setting, TFile } from 'obsidian'; +import { App, Notice, PluginSettingTab, Setting, TFile, SuggestModal } from 'obsidian'; import Khoj from 'src/main'; import { canConnectToBackend, getBackendStatusMessage, updateContentIndex } from './utils'; @@ -15,6 +15,7 @@ interface SyncFileTypes { images: boolean; pdf: boolean; } + export interface KhojSetting { resultsCount: number; khojUrl: string; @@ -24,6 +25,7 @@ export interface KhojSetting { lastSync: Map; syncFileType: SyncFileTypes; userInfo: UserInfo | null; + syncFolders: string[]; } export const DEFAULT_SETTINGS: KhojSetting = { @@ -39,6 +41,7 @@ export const DEFAULT_SETTINGS: KhojSetting = { pdf: true, }, userInfo: null, + syncFolders: [], } export class KhojSettingTab extends PluginSettingTab { @@ -60,7 +63,8 @@ export class KhojSettingTab extends PluginSettingTab { this.plugin.settings.userInfo?.email, this.plugin.settings.khojUrl, this.plugin.settings.khojApiKey - )} + ) + } ); let backendStatusMessage: string = ''; @@ -109,7 +113,7 @@ export class KhojSettingTab extends PluginSettingTab { })); // Add new "Sync" heading - containerEl.createEl('h3', {text: 'Sync'}); + containerEl.createEl('h3', { text: 'Sync' }); // Add setting to sync markdown notes new Setting(containerEl) @@ -153,6 +157,29 @@ export class KhojSettingTab extends PluginSettingTab { this.plugin.settings.autoConfigure = value; await this.plugin.saveSettings(); })); + + // Add setting to manage sync folders + const syncFoldersContainer = containerEl.createDiv('sync-folders-container'); + const foldersSetting = new Setting(syncFoldersContainer) + .setName('Sync Folders') + .setDesc('Specify folders to sync (leave empty to sync entire vault)') + .addButton(button => button + .setButtonText('Add Folder') + .onClick(() => { + const modal = new FolderSuggestModal(this.app, (folder: string) => { + if (!this.plugin.settings.syncFolders.includes(folder)) { + this.plugin.settings.syncFolders.push(folder); + this.plugin.saveSettings(); + this.updateFolderList(folderListEl); + } + }); + modal.open(); + })); + + // Create a list to display selected folders + const folderListEl = syncFoldersContainer.createDiv('folder-list'); + this.updateFolderList(folderListEl); + let indexVaultSetting = new Setting(containerEl); indexVaultSetting .setName('Force Sync') @@ -200,4 +227,81 @@ export class KhojSettingTab extends PluginSettingTab { }) ); } + + // Helper method to update the folder list display + private updateFolderList(containerEl: HTMLElement) { + containerEl.empty(); + if (this.plugin.settings.syncFolders.length === 0) { + containerEl.createEl('div', { + text: 'Syncing entire vault', + cls: 'folder-list-empty' + }); + return; + } + + const list = containerEl.createEl('ul', { cls: 'folder-list' }); + this.plugin.settings.syncFolders.forEach(folder => { + const item = list.createEl('li', { cls: 'folder-list-item' }); + item.createSpan({ text: folder }); + + const removeButton = item.createEl('button', { + cls: 'folder-list-remove', + text: '×' + }); + removeButton.addEventListener('click', async () => { + this.plugin.settings.syncFolders = this.plugin.settings.syncFolders.filter(f => f !== folder); + await this.plugin.saveSettings(); + this.updateFolderList(containerEl); + }); + }); + } +} + +// Modal with folder suggestions +class FolderSuggestModal extends SuggestModal { + constructor(app: App, private onChoose: (folder: string) => void) { + super(app); + } + + getSuggestions(query: string): string[] { + const folders = this.getAllFolders(); + if (!query) return folders; + + return folders.filter(folder => + folder.toLowerCase().includes(query.toLowerCase()) + ); + } + + renderSuggestion(folder: string, el: HTMLElement) { + el.createSpan({ + text: folder || '/', + cls: 'folder-suggest-item' + }); + } + + onChooseSuggestion(folder: string, _: MouseEvent | KeyboardEvent) { + this.onChoose(folder); + } + + private getAllFolders(): string[] { + const folders = new Set(); + folders.add(''); // Root folder + + // Récupérer tous les fichiers et extraire les chemins des dossiers + this.app.vault.getAllLoadedFiles().forEach(file => { + const folderPath = file.parent?.path; + if (folderPath) { + folders.add(folderPath); + + // Ajouter aussi tous les dossiers parents + let parent = folderPath; + while (parent.includes('/')) { + parent = parent.substring(0, parent.lastIndexOf('/')); + folders.add(parent); + } + } + }); + + return Array.from(folders).sort(); + } } diff --git a/src/interface/obsidian/src/utils.ts b/src/interface/obsidian/src/utils.ts index 3cc83fb88..b0dbf61f6 100644 --- a/src/interface/obsidian/src/utils.ts +++ b/src/interface/obsidian/src/utils.ts @@ -9,7 +9,7 @@ export function getVaultAbsolutePath(vault: Vault): string { return ''; } -function fileExtensionToMimeType (extension: string): string { +function fileExtensionToMimeType(extension: string): string { switch (extension) { case 'pdf': return 'application/pdf'; @@ -28,7 +28,7 @@ function fileExtensionToMimeType (extension: string): string { } } -function filenameToMimeType (filename: TFile): string { +function filenameToMimeType(filename: TFile): string { switch (filename.extension) { case 'pdf': return 'application/pdf'; @@ -63,15 +63,24 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las // Get all markdown, pdf files in the vault console.log(`Khoj: Updating Khoj content index...`) const files = vault.getFiles() - // Filter supported file types for syncing - .filter(file => supportedFileTypes.includes(file.extension)) - // Filter user configured file types for syncing - .filter(file => { - if (fileTypeToExtension.markdown.includes(file.extension)) return setting.syncFileType.markdown; - if (fileTypeToExtension.pdf.includes(file.extension)) return setting.syncFileType.pdf; - if (fileTypeToExtension.image.includes(file.extension)) return setting.syncFileType.images; - return false; - }); + // Filter supported file types for syncing + .filter(file => supportedFileTypes.includes(file.extension)) + // Filter user configured file types for syncing + .filter(file => { + if (fileTypeToExtension.markdown.includes(file.extension)) return setting.syncFileType.markdown; + if (fileTypeToExtension.pdf.includes(file.extension)) return setting.syncFileType.pdf; + if (fileTypeToExtension.image.includes(file.extension)) return setting.syncFileType.images; + return false; + }) + // Filter files based on specified folders + .filter(file => { + // Si aucun dossier n'est spécifié, synchroniser tous les fichiers + if (setting.syncFolders.length === 0) return true; + // Sinon, vérifier si le fichier est dans un des dossiers spécifiés + return setting.syncFolders.some(folder => + file.path.startsWith(folder + '/') || file.path === folder + ); + }); let countOfFilesToIndex = 0; let countOfFilesToDelete = 0; @@ -81,7 +90,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las const fileData = []; for (const file of files) { // Only push files that have been modified since last sync if not regenerating - if (!regenerate && file.stat.mtime < (lastSync.get(file) ?? 0)){ + if (!regenerate && file.stat.mtime < (lastSync.get(file) ?? 0)) { continue; } @@ -89,7 +98,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las const encoding = supportedBinaryFileTypes.includes(file.extension) ? "binary" : "utf8"; const mimeType = fileExtensionToMimeType(file.extension) + (encoding === "utf8" ? "; charset=UTF-8" : ""); const fileContent = encoding == 'binary' ? await vault.readBinary(file) : await vault.read(file); - fileData.push({blob: new Blob([fileContent], { type: mimeType }), path: file.path}); + fileData.push({ blob: new Blob([fileContent], { type: mimeType }), path: file.path }); } // Add any previously synced files to be deleted to multipart form data @@ -98,13 +107,13 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las if (!files.includes(lastSyncedFile)) { countOfFilesToDelete++; let fileObj = new Blob([""], { type: filenameToMimeType(lastSyncedFile) }); - fileData.push({blob: fileObj, path: lastSyncedFile.path}); + fileData.push({ blob: fileObj, path: lastSyncedFile.path }); filesToDelete.push(lastSyncedFile); } } // Iterate through all indexable files in vault, 1000 at a time - let responses: string[] = []; + let responses: string[] = []; let error_message = null; for (let i = 0; i < fileData.length; i += 1000) { const filesGroup = fileData.slice(i, i + 1000); @@ -166,17 +175,17 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las } // Update last sync time for each successfully indexed file - files - .filter(file => responses.find(response => response.includes(file.path))) - .reduce((newSync, file) => { - newSync.set(file, new Date().getTime()); - return newSync; - }, lastSync); + files + .filter(file => responses.find(response => response.includes(file.path))) + .reduce((newSync, file) => { + newSync.set(file, new Date().getTime()); + return newSync; + }, lastSync); // Remove files that were deleted from last sync filesToDelete - .filter(file => responses.find(response => response.includes(file.path))) - .forEach(file => lastSync.delete(file)); + .filter(file => responses.find(response => response.includes(file.path))) + .forEach(file => lastSync.delete(file)); if (error_message) { new Notice(error_message); @@ -188,31 +197,30 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las return lastSync; } -export async function openKhojPluginSettings(): Promise - { - const setting = this.app.setting; - await setting.open(); - setting.openTabById('khoj'); +export async function openKhojPluginSettings(): Promise { + const setting = this.app.setting; + await setting.open(); + setting.openTabById('khoj'); } export async function createNote(name: string, newLeaf = false): Promise { try { - let pathPrefix: string - switch (this.app.vault.getConfig('newFileLocation')) { - case 'current': - pathPrefix = (this.app.workspace.getActiveFile()?.parent.path ?? '') + '/' - break - case 'folder': - pathPrefix = this.app.vault.getConfig('newFileFolderPath') + '/' - break - default: // 'root' - pathPrefix = '' - break - } - await this.app.workspace.openLinkText(`${pathPrefix}${name}.md`, '', newLeaf) + let pathPrefix: string + switch (this.app.vault.getConfig('newFileLocation')) { + case 'current': + pathPrefix = (this.app.workspace.getActiveFile()?.parent.path ?? '') + '/' + break + case 'folder': + pathPrefix = this.app.vault.getConfig('newFileFolderPath') + '/' + break + default: // 'root' + pathPrefix = '' + break + } + await this.app.workspace.openLinkText(`${pathPrefix}${name}.md`, '', newLeaf) } catch (e) { - console.error('Khoj: Could not create note.\n' + (e as any).message); - throw e + console.error('Khoj: Could not create note.\n' + (e as any).message); + throw e } } @@ -236,7 +244,7 @@ export async function canConnectToBackend( let userInfo: UserInfo | null = null; if (!!khojUrl) { - let headers = !!khojApiKey ? { "Authorization": `Bearer ${khojApiKey}` } : undefined; + let headers = !!khojApiKey ? { "Authorization": `Bearer ${khojApiKey}` } : undefined; try { let response = await request({ url: `${khojUrl}/api/v1/user`, method: "GET", headers: headers }) connectedToBackend = true; @@ -387,7 +395,7 @@ function copyParentText(event: MouseEvent, message: string, originalButton: stri } export function createCopyParentText(message: string, originalButton: string = 'copy-plus') { - return function(event: MouseEvent) { + return function (event: MouseEvent) { return copyParentText(event, message, originalButton); } } @@ -406,7 +414,7 @@ export function pasteTextAtCursor(text: string | undefined) { // If there is a selection, replace it with the text if (editor?.getSelection()) { editor.replaceSelection(text); - // If there is no selection, insert the text at the cursor position + // If there is no selection, insert the text at the cursor position } else if (cursor) { editor.replaceRange(text, cursor); } diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 0d2ea1d4d..35573b0c4 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -13,11 +13,12 @@ If your plugin does not need CSS, delete this file. --khoj-storm-grey: #475569; --chat-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='currentColor' stroke-linecap='round' stroke-linejoin='round' class='svg-icon' version='1.1'%3E%3Cpath d='m 14.024348,9.8497703 0.04627,1.9750167' stroke='%231c274c' stroke-width='2' stroke-linecap='round' /%3E%3Cpath d='m 9.6453624,9.7953624 0.046275,1.9750166' stroke='%231c274c' stroke-width='2' stroke-linecap='round' /%3E%3Cpath d='m 11.90538,2.3619994 c -5.4939109,0 -9.6890976,4.0608185 -9.6890976,9.8578926 0,1.477202 0.2658016,2.542848 0.6989332,3.331408 0.433559,0.789293 1.0740097,1.372483 1.9230615,1.798517 1.7362861,0.87132 4.1946007,1.018626 7.0671029,1.018626 0.317997,0 0.593711,0.167879 0.784844,0.458501 0.166463,0.253124 0.238617,0.552748 0.275566,0.787233 0.07263,0.460801 0.05871,1.030165 0.04785,1.474824 v 4.8e-5 l -2.26e-4,0.0091 c -0.0085,0.348246 -0.01538,0.634247 -0.0085,0.861186 0.105589,-0.07971 0.227925,-0.185287 0.36735,-0.31735 0.348613,-0.330307 0.743513,-0.767362 1.176607,-1.246635 l 0.07837,-0.08673 c 0.452675,-0.500762 0.941688,-1.037938 1.41216,-1.473209 0.453774,-0.419787 0.969948,-0.822472 1.476003,-0.953853 1.323661,-0.343655 2.330132,-0.904027 3.005749,-1.76381 0.658957,-0.838568 1.073167,-2.051868 1.073167,-3.898667 0,-5.7970748 -4.195186,-9.8578946 -9.689097,-9.8578946 z M 0.92440678,12.219892 c 0,-7.0067939 5.05909412,-11.47090892 10.98097322,-11.47090892 5.921878,0 10.980972,4.46411502 10.980972,11.47090892 0,2.172259 -0.497596,3.825405 -1.442862,5.028357 -0.928601,1.181693 -2.218843,1.837914 -3.664937,2.213334 -0.211641,0.05502 -0.53529,0.268579 -0.969874,0.670658 -0.417861,0.386604 -0.865628,0.876836 -1.324566,1.384504 l -0.09131,0.101202 c -0.419252,0.464136 -0.849637,0.94059 -1.239338,1.309807 -0.210187,0.199169 -0.425281,0.383422 -0.635348,0.523424 -0.200911,0.133819 -0.449635,0.263369 -0.716376,0.281474 -0.327812,0.02226 -0.61539,-0.149209 -0.804998,-0.457293 -0.157614,-0.255993 -0.217622,-0.557143 -0.246564,-0.778198 -0.0542,-0.414027 -0.04101,-0.933065 -0.03027,-1.355183 l 0.0024,-0.0922 c 0.01099,-0.463865 0.01489,-0.820507 -0.01611,-1.06842 C 8.9434608,19.975238 6.3139711,19.828758 4.356743,18.84659 3.3355029,18.334136 2.4624526,17.578678 1.8500164,16.463713 1.2372016,15.348029 0.92459928,13.943803 0.92459928,12.219967 Z' clip-rule='evenodd' stroke-width='2' fill='currentColor' fill-rule='evenodd' fill-opacity='1' /%3E%3C/svg%3E%0A"); --search-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24px' height='24px' viewBox='0 0 24 24' fill='currentColor' stroke-linecap='round' stroke-linejoin='round' class='svg-icon' version='1.1'%3E%3Cpath d='m 18.562765,17.147843 c 1.380497,-1.679442 2.307667,-4.013099 2.307667,-6.330999 C 20.870432,5.3951476 16.353958,1 10.782674,1 5.2113555,1 0.69491525,5.3951476 0.69491525,10.816844 c 0,5.421663 4.51644025,9.816844 10.08775875,9.816844 2.381867,0 4.570922,-0.803307 6.296712,-2.14673 0.508475,-0.508475 4.514633,4.192839 4.514633,4.192839 1.036377,1.008544 2.113087,-0.02559 1.07671,-1.034139 z m -7.780091,1.925408 c -4.3394583,0 -8.6708434,-4.033489 -8.6708434,-8.256407 0,-4.2229187 4.3313851,-8.2564401 8.6708434,-8.2564401 4.339458,0 8.670809,4.2369112 8.670809,8.4598301 0,4.222918 -4.331351,8.053017 -8.670809,8.053017 z' fill='currentColor' fill-rule='evenodd' clip-rule='evenodd' fill-opacity='1' stroke-width='1.10519' stroke-dasharray='none' /%3E%3Cpath d='m 13.337351,9.3402647 0.05184,2.1532893' stroke='%231c274c' stroke-width='2' stroke-linecap='round' /%3E%3Cpath d='M 8.431347,9.2809457 8.483191,11.434235' stroke='%231c274c' stroke-width='2' stroke-linecap='round' /%3E%3C/svg%3E%0A"); - } +} .khoj-chat p { margin: 0; } + .khoj-chat pre { text-wrap: unset; } @@ -33,7 +34,8 @@ If your plugin does not need CSS, delete this file. font-weight: 300; line-height: 1.5em; } -.khoj-chat > * { + +.khoj-chat>* { padding: 10px; margin: 10px; } @@ -47,8 +49,10 @@ If your plugin does not need CSS, delete this file. font-size: var(--font-ui-medium); margin: 0px; line-height: 20px; - overflow-y: scroll; /* Make chat body scroll to see history */ + overflow-y: scroll; + /* Make chat body scroll to see history */ } + /* add chat metatdata to bottom of bubble */ .khoj-chat-message.khoj::after { content: attr(data-meta); @@ -57,16 +61,19 @@ If your plugin does not need CSS, delete this file. color: var(--text-muted); margin: -12px 7px 0 0px; } + /* move message by khoj to left */ .khoj-chat-message.khoj { margin-left: auto; text-align: left; } + /* move message by you to right */ .khoj-chat-message.you { margin-right: auto; text-align: right; } + /* basic style chat message text */ .khoj-chat-message-text { margin: 10px; @@ -80,6 +87,7 @@ If your plugin does not need CSS, delete this file. background-color: var(--active-bg); word-break: break-word; } + /* color chat bubble by khoj blue */ .khoj-chat-message-text.khoj { border-left: 2px solid var(--khoj-sun); @@ -87,12 +95,14 @@ If your plugin does not need CSS, delete this file. margin-left: auto; white-space: pre-line; } + /* Override white-space for ul, ol, li under khoj-chat-message-text.khoj */ .khoj-chat-message-text.khoj ul, .khoj-chat-message-text.khoj ol, .khoj-chat-message-text.khoj li { white-space: normal; } + /* add left protrusion to khoj chat bubble */ .khoj-chat-message-text.khoj:after { content: ''; @@ -103,12 +113,14 @@ If your plugin does not need CSS, delete this file. border-bottom: 0; transform: rotate(-60deg); } + /* color chat bubble by you dark grey */ .khoj-chat-message-text.you { color: var(--text-normal); margin-right: auto; background-color: var(--background-modifier-cover); } + /* add right protrusion to you chat bubble */ .khoj-chat-message-text.you:after { content: ''; @@ -125,6 +137,7 @@ If your plugin does not need CSS, delete this file. .khoj-chat-message-text ol { margin: 0px 0 0; } + .khoj-chat-message-text ol li { white-space: normal; } @@ -146,9 +159,11 @@ code.chat-response { div.collapsed { display: none; } + div.expanded { display: block; } + div.reference { display: grid; grid-template-rows: auto; @@ -157,6 +172,7 @@ div.reference { grid-row-gap: 10px; margin: 10px; } + div.expanded.reference-section { display: grid; grid-template-rows: auto; @@ -165,6 +181,7 @@ div.expanded.reference-section { grid-row-gap: 10px; margin: 10px 0; } + button.reference-button { border: 1px solid var(--khoj-storm-grey); background-color: transparent; @@ -183,15 +200,18 @@ button.reference-button { display: inline-block; text-wrap: inherit; } + button.reference-button.expanded { height: auto; max-height: none; white-space: pre-wrap; } -button.reference-button.expanded > :nth-child(2) { + +button.reference-button.expanded> :nth-child(2) { display: block; } -button.reference-button.collapsed > :nth-child(2) { + +button.reference-button.collapsed> :nth-child(2) { display: none; } @@ -201,11 +221,13 @@ button.reference-button::before { display: inline-block; transition: transform 0.1s ease-in-out; } + button.reference-button.expanded::before, button.reference-button:active:before, button.reference-button[aria-expanded="true"]::before { transform: rotate(90deg); } + button.reference-expand-button { background-color: transparent; border: 1px solid var(--khoj-storm-grey); @@ -219,15 +241,18 @@ button.reference-expand-button { transition: background 0.2s ease-in-out; text-align: left; } + button.reference-expand-button:hover { background: var(--background-modifier-active-hover); color: var(--text-normal); } + a.inline-chat-link { color: #475569; text-decoration: none; border-bottom: 1px dotted #475569; } + .reference-link { color: var(--khoj-storm-grey); border-bottom: 1px dotted var(--khoj-storm-grey); @@ -247,11 +272,13 @@ div.new-conversation { z-index: 10; background-color: var(--background-primary) } + div.conversation-header-title { text-align: left; font-size: larger; line-height: 1.5em; } + div.conversation-session { color: var(--color-base-90); border: 1px solid var(--khoj-storm-grey); @@ -298,9 +325,11 @@ div.conversation-menu { grid-gap: 4px; grid-auto-flow: column; } + div.conversation-session:hover { transform: scale(1.03); } + div.selected-conversation { background: var(--background-modifier-active-hover) !important; } @@ -312,6 +341,7 @@ div.selected-conversation { grid-column-gap: 10px; grid-row-gap: 10px; } + .khoj-input-row { display: grid; grid-template-columns: 32px auto 32px 32px; @@ -324,9 +354,11 @@ div.selected-conversation { bottom: 0; z-index: 10; } + #khoj-chat-input.option:hover { box-shadow: 0 0 11px var(--background-modifier-box-shadow); } + #khoj-chat-input { font-size: var(--font-ui-medium); padding: 4px 0 0 12px; @@ -334,6 +366,7 @@ div.selected-conversation { height: 32px; resize: none; } + .khoj-input-row-button { border-radius: 50%; padding: 4px; @@ -346,43 +379,55 @@ div.selected-conversation { padding: 0; position: relative; } + #khoj-chat-send .lucide-arrow-up-circle { background: var(--background-modifier-active-hover); border-radius: 50%; } + #khoj-chat-send .lucide-stop-circle { transform: rotateY(-180deg) rotateZ(-90deg); } + #khoj-chat-send .lucide-stop-circle circle { - stroke-dasharray: 62px; /* The circumference of the circle with 7px radius */ + stroke-dasharray: 62px; + /* The circumference of the circle with 7px radius */ stroke-dashoffset: 0px; stroke-linecap: round; stroke-width: 2px; stroke: var(--main-text-color); fill: none; } + @keyframes countdown { from { stroke-dashoffset: 0px; } + to { - stroke-dashoffset: -62px; /* The circumference of the circle with 7px radius */ + stroke-dashoffset: -62px; + /* The circumference of the circle with 7px radius */ } } -@media (pointer: coarse), (hover: none) { +@media (pointer: coarse), +(hover: none) { #khoj-chat-body.abbr[title] { position: relative; - padding-left: 4px; /* space references out to ease tapping */ + padding-left: 4px; + /* space references out to ease tapping */ } + #khoj-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 */ + 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 */ + z-index: 1; + /* show tooltip above chat messages */ /* style tooltip */ background-color: var(--background-secondary); @@ -410,11 +455,11 @@ div.selected-conversation { white-space: normal; } -.khoj-result-entry > * { +.khoj-result-entry>* { font-size: var(--font-ui-medium); } -.khoj-result-entry > p { +.khoj-result-entry>p { margin-top: 0.2em; margin-bottom: 0.2em; } @@ -440,9 +485,11 @@ div.khoj-header { a.khoj-nav { -webkit-app-region: no-drag; } + div.khoj-nav { -webkit-app-region: no-drag; } + nav.khoj-nav { display: grid; grid-auto-flow: column; @@ -470,24 +517,30 @@ div.khoj-logo { justify-self: center; margin: 0; } + .khoj-nav a:hover { background-color: var(--background-modifier-active-hover); color: var(--main-text-color); } + a.khoj-nav-selected { background-color: var(--background-modifier-active-hover); } + #similar-nav-icon-svg, .khoj-nav-icon { width: 24px; height: 24px; } + .khoj-nav-icon-chat { background-image: var(--chat-icon); } + .khoj-nav-icon-search { background-image: var(--search-icon); } + span.khoj-nav-item-text { padding-left: 8px; } @@ -507,12 +560,14 @@ button.chat-action-button { margin-top: 8px; float: right; } + button.chat-action-button span { cursor: pointer; display: inline-block; position: relative; transition: 0.5s; } + button.chat-action-button:hover { background-color: var(--background-modifier-active-hover); color: var(--text-normal); @@ -534,6 +589,7 @@ img.copy-icon { box-sizing: border-box; animation: rotation 1s linear infinite; } + .loader::after { content: ''; box-sizing: border-box; @@ -552,6 +608,7 @@ img.copy-icon { 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } @@ -564,6 +621,7 @@ img.copy-icon { width: 60px; height: 32px; } + .lds-ellipsis div { position: absolute; top: 12px; @@ -573,42 +631,52 @@ img.copy-icon { background: var(--color-base-70); animation-timing-function: cubic-bezier(0, 1, 1, 0); } + .lds-ellipsis div:nth-child(1) { left: 8px; animation: lds-ellipsis1 0.6s infinite; } + .lds-ellipsis div:nth-child(2) { left: 8px; animation: lds-ellipsis2 0.6s infinite; } + .lds-ellipsis div:nth-child(3) { left: 32px; animation: lds-ellipsis2 0.6s infinite; } + .lds-ellipsis div:nth-child(4) { left: 56px; animation: lds-ellipsis3 0.6s infinite; } + @keyframes lds-ellipsis1 { 0% { transform: scale(0); } + 100% { transform: scale(1); } } + @keyframes lds-ellipsis3 { 0% { transform: scale(1); } + 100% { transform: scale(0); } } + @keyframes lds-ellipsis2 { 0% { transform: translate(0, 0); } + 100% { transform: translate(24px, 0); } @@ -633,15 +701,18 @@ img.copy-icon { border-radius: 50%; animation: pulse 3s ease-in-out infinite; } + @keyframes pulse { 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.2); opacity: 0.2; } + 100% { transform: scale(1); opacity: 1; @@ -649,9 +720,15 @@ img.copy-icon { } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } + @media only screen and (max-width: 600px) { div.khoj-header { display: grid; @@ -665,10 +742,65 @@ img.copy-icon { grid-gap: 0px; justify-content: space-between; } + a.khoj-nav { padding: 0 16px; } + span.khoj-nav-item-text { display: none; } } + +/* Folder list styles */ +.folder-list { + list-style: none; + padding: 0; + margin: 8px 0; +} + +.folder-list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + margin: 4px 0; + background: var(--background-secondary); + border-radius: 4px; + min-height: 32px; +} + +.folder-list-remove { + background: none; + border: none; + color: #ff5555; + cursor: pointer; + font-size: 18px; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + margin-left: 8px; + padding: 0; + border-radius: 4px; + opacity: 0.7; + transition: all 0.2s ease; +} + +.folder-list-remove:hover { + opacity: 1; + background-color: rgba(255, 85, 85, 0.1); +} + +.folder-list-empty { + color: var(--text-muted); + font-style: italic; + padding: 6px 0; +} + +/* Folder suggestion modal styles */ +.folder-suggest-item { + padding: 4px 8px; + display: block; +} \ No newline at end of file From 7d28b46ca7631be860f5c3b4c25da7c033f9417a Mon Sep 17 00:00:00 2001 From: Henri Jamet <42291955+hjamet@users.noreply.github.com> Date: Sun, 29 Dec 2024 13:23:08 +0100 Subject: [PATCH 3/5] Implement sync interval setting and enhance synchronization timer in Khoj plugin - Added a new setting for users to configure the sync interval in minutes, allowing for more flexible automatic synchronization. - Introduced methods to start and restart the synchronization timer based on the configured interval. - Updated the synchronization logic to use the user-defined interval instead of a fixed 60 minutes. - Improved code readability and organization by refactoring the sync timer logic. --- src/interface/obsidian/src/main.ts | 32 +++++++++++++++++++++----- src/interface/obsidian/src/settings.ts | 24 +++++++++++++++++++ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index f5f80cfc4..bb4eecbd5 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -43,8 +43,8 @@ export default class Khoj extends Plugin { this.app.vault, this.settings, this.settings.lastSync, - false, // regenerate = false pour ne synchroniser que les nouvelles modifications - true // userTriggered = true pour afficher une notification + false, + true ); } }); @@ -59,12 +59,32 @@ export default class Khoj extends Plugin { // Add a settings tab so the user can configure khoj this.addSettingTab(new KhojSettingTab(this.app, this)); - // Add scheduled job to update index every 60 minutes + // Démarrer le timer de synchronisation + this.startSyncTimer(); + } + + // Méthode pour démarrer le timer de synchronisation + private startSyncTimer() { + // Nettoyer l'ancien timer s'il existe + if (this.indexingTimer) { + clearInterval(this.indexingTimer); + } + + // Démarrer un nouveau timer avec l'intervalle configuré this.indexingTimer = setInterval(async () => { if (this.settings.autoConfigure) { - this.settings.lastSync = await updateContentIndex(this.app.vault, this.settings, this.settings.lastSync); + this.settings.lastSync = await updateContentIndex( + this.app.vault, + this.settings, + this.settings.lastSync + ); } - }, 60 * 60 * 1000); + }, this.settings.syncInterval * 60 * 1000); // Convertir les minutes en millisecondes + } + + // Méthode publique pour redémarrer le timer (appelée depuis les paramètres) + public restartSyncTimer() { + this.startSyncTimer(); } async loadSettings() { @@ -77,7 +97,7 @@ export default class Khoj extends Plugin { } async saveSettings() { - this.saveData(this.settings); + await this.saveData(this.settings); } async onunload() { diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index 62467b88a..54dbb812b 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -26,6 +26,7 @@ export interface KhojSetting { syncFileType: SyncFileTypes; userInfo: UserInfo | null; syncFolders: string[]; + syncInterval: number; } export const DEFAULT_SETTINGS: KhojSetting = { @@ -42,6 +43,7 @@ export const DEFAULT_SETTINGS: KhojSetting = { }, userInfo: null, syncFolders: [], + syncInterval: 60, } export class KhojSettingTab extends PluginSettingTab { @@ -158,6 +160,28 @@ export class KhojSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + // Add setting for sync interval + const syncIntervalValues = [1, 5, 10, 20, 30, 45, 60, 120, 1440]; + new Setting(containerEl) + .setName('Sync Interval') + .setDesc('Minutes between automatic synchronizations') + .addDropdown(dropdown => dropdown + .addOptions(Object.fromEntries( + syncIntervalValues.map(value => [ + value.toString(), + value === 1 ? '1 minute' : + value === 1440 ? '24 hours' : + `${value} minutes` + ]) + )) + .setValue(this.plugin.settings.syncInterval.toString()) + .onChange(async (value) => { + this.plugin.settings.syncInterval = parseInt(value); + await this.plugin.saveSettings(); + // Redémarrer le timer avec le nouvel intervalle + this.plugin.restartSyncTimer(); + })); + // Add setting to manage sync folders const syncFoldersContainer = containerEl.createDiv('sync-folders-container'); const foldersSetting = new Setting(syncFoldersContainer) From 1aff78a969deb83a70232bc3352c02c24ae45c1b Mon Sep 17 00:00:00 2001 From: Henri Jamet <42291955+hjamet@users.noreply.github.com> Date: Sun, 29 Dec 2024 16:19:42 +0100 Subject: [PATCH 4/5] Enhance Khoj plugin search functionality and loading indicators - Added visual loading indicators to the search modal for improved user experience during search operations. - Implemented logic to check if search results correspond to files in the vault, with color-coded results for better clarity. - Refactored the getSuggestions method to handle loading states and abort previous requests if necessary. - Updated CSS styles to support new loading animations and result file status indicators. - Improved the renderSuggestion method to display file status and provide feedback for files not in the vault. --- src/interface/obsidian/src/search_modal.ts | 164 ++++++++++++++++++--- src/interface/obsidian/styles.css | 55 +++++++ 2 files changed, 195 insertions(+), 24 deletions(-) diff --git a/src/interface/obsidian/src/search_modal.ts b/src/interface/obsidian/src/search_modal.ts index 60b4accbc..0e1093ed9 100644 --- a/src/interface/obsidian/src/search_modal.ts +++ b/src/interface/obsidian/src/search_modal.ts @@ -1,4 +1,4 @@ -import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform } from 'obsidian'; +import { App, SuggestModal, request, MarkdownRenderer, Instruction, Platform, Notice } from 'obsidian'; import { KhojSetting } from 'src/settings'; import { supportedBinaryFileTypes, createNoteAndCloseModal, getFileFromPath, getLinkToEntry, supportedImageFilesTypes } from 'src/utils'; @@ -13,6 +13,9 @@ export class KhojSearchModal extends SuggestModal { find_similar_notes: boolean; query: string = ""; app: App; + currentController: AbortController | null = null; // Pour annuler les requêtes + isLoading: boolean = false; + loadingEl: HTMLElement; constructor(app: App, setting: KhojSetting, find_similar_notes: boolean = false) { super(app); @@ -23,6 +26,24 @@ export class KhojSearchModal extends SuggestModal { // Hide input element in Similar Notes mode this.inputEl.hidden = this.find_similar_notes; + // Create loading element + this.loadingEl = createDiv({ cls: "search-loading" }); + const spinnerEl = this.loadingEl.createDiv({ cls: "search-loading-spinner" }); + + this.loadingEl.style.position = "absolute"; + this.loadingEl.style.top = "50%"; + this.loadingEl.style.left = "50%"; + this.loadingEl.style.transform = "translate(-50%, -50%)"; + this.loadingEl.style.zIndex = "1000"; + this.loadingEl.style.display = "none"; + + // Ajouter l'élément au modal + this.modalEl.appendChild(this.loadingEl); + + // Customize empty state message + // @ts-ignore - Accès à la propriété privée pour personnaliser le message + this.emptyStateText = ""; + // Register Modal Keybindings to Rerank Results this.scope.register(['Mod'], 'Enter', async () => { // Re-rank when explicitly triggered by user @@ -66,6 +87,101 @@ export class KhojSearchModal extends SuggestModal { this.setPlaceholder('Search with Khoj...'); } + // Check if the file exists in the vault + private isFileInVault(filePath: string): boolean { + // Normalize the path to handle different separators + const normalizedPath = filePath.replace(/\\/g, '/'); + + // Check if the file exists in the vault + return this.app.vault.getFiles().some(file => + file.path === normalizedPath + ); + } + + async getSuggestions(query: string): Promise { + // Ne pas afficher le chargement si la requête est vide + if (!query.trim()) { + this.isLoading = false; + this.updateLoadingState(); + return []; + } + + // Show loading state + this.isLoading = true; + this.updateLoadingState(); + + // Cancel previous request if it exists + if (this.currentController) { + this.currentController.abort(); + } + + try { + // Create a new controller for this request + this.currentController = new AbortController(); + + // Setup Query Khoj backend for search results + let encodedQuery = encodeURIComponent(query); + let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=${this.rerank}&client=obsidian`; + let headers = { + 'Authorization': `Bearer ${this.setting.khojApiKey}`, + } + + // Get search results from Khoj backend + const response = await fetch(searchUrl, { + headers: headers, + signal: this.currentController.signal + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Parse search results + let results = data + .filter((result: any) => + !this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path) + ) + .map((result: any) => { + return { + entry: result.entry, + file: result.additional.file, + inVault: this.isFileInVault(result.additional.file) + } as SearchResult & { inVault: boolean }; + }) + .sort((a: SearchResult & { inVault: boolean }, b: SearchResult & { inVault: boolean }) => { + if (a.inVault === b.inVault) return 0; + return a.inVault ? -1 : 1; + }); + + this.query = query; + + // Hide loading state only on successful completion + this.isLoading = false; + this.updateLoadingState(); + + return results; + } catch (error) { + // Ignore cancellation errors and keep loading state + if (error.name === 'AbortError') { + // When cancelling, we don't want to render anything + return undefined as any; + } + + // For other errors, hide loading state + console.error('Search error:', error); + this.isLoading = false; + this.updateLoadingState(); + return []; + } + } + + private updateLoadingState() { + // Show or hide loading element + this.loadingEl.style.display = this.isLoading ? "block" : "none"; + } + async onOpen() { if (this.find_similar_notes) { // If markdown file is currently active @@ -86,25 +202,7 @@ export class KhojSearchModal extends SuggestModal { } } - async getSuggestions(query: string): Promise { - // Setup Query Khoj backend for search results - let encodedQuery = encodeURIComponent(query); - let searchUrl = `${this.setting.khojUrl}/api/search?q=${encodedQuery}&n=${this.setting.resultsCount}&r=${this.rerank}&client=obsidian`; - let headers = { 'Authorization': `Bearer ${this.setting.khojApiKey}` } - - // Get search results from Khoj backend - let response = await request({ url: `${searchUrl}`, headers: headers }); - - // Parse search results - let results = JSON.parse(response) - .filter((result: any) => !this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path)) - .map((result: any) => { return { entry: result.entry, file: result.additional.file } as SearchResult; }); - - this.query = query; - return results; - } - - async renderSuggestion(result: SearchResult, el: HTMLElement) { + async renderSuggestion(result: SearchResult & { inVault: boolean }, el: HTMLElement) { // Max number of lines to render let lines_to_render = 8; @@ -112,13 +210,25 @@ export class KhojSearchModal extends SuggestModal { let os_path_separator = result.file.includes('\\') ? '\\' : '/'; let filename = result.file.split(os_path_separator).pop(); - // Show filename of each search result for context - el.createEl("div",{ cls: 'khoj-result-file' }).setText(filename ?? ""); + // Show filename of each search result for context with appropriate color + const fileEl = el.createEl("div", { + cls: `khoj-result-file ${result.inVault ? 'in-vault' : 'not-in-vault'}` + }); + fileEl.setText(filename ?? ""); + + // Add a visual indication for files not in vault + if (!result.inVault) { + fileEl.createSpan({ + text: " (not in vault)", + cls: "khoj-result-file-status" + }); + } + let result_el = el.createEl("div", { cls: 'khoj-result-entry' }) let resultToRender = ""; let fileExtension = filename?.split(".").pop() ?? ""; - if (supportedImageFilesTypes.includes(fileExtension) && filename) { + if (supportedImageFilesTypes.includes(fileExtension) && filename && result.inVault) { let linkToEntry: string = filename; let imageFiles = this.app.vault.getFiles().filter(file => supportedImageFilesTypes.includes(fileExtension)); // Find vault file of chosen search result @@ -140,7 +250,13 @@ export class KhojSearchModal extends SuggestModal { MarkdownRenderer.renderMarkdown(resultToRender, result_el, result.file, null); } - async onChooseSuggestion(result: SearchResult, _: MouseEvent | KeyboardEvent) { + async onChooseSuggestion(result: SearchResult & { inVault: boolean }, _: MouseEvent | KeyboardEvent) { + // Only open files that are in the vault + if (!result.inVault) { + new Notice("This file is not in your vault"); + return; + } + // Get all markdown, pdf and image files in vault const mdFiles = this.app.vault.getMarkdownFiles(); const binaryFiles = this.app.vault.getFiles().filter(file => supportedBinaryFileTypes.includes(file.extension)); diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 35573b0c4..728c064f9 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -443,6 +443,14 @@ div.selected-conversation { font-weight: 600; } +.khoj-result-file.in-vault { + color: var(--color-green); +} + +.khoj-result-file.not-in-vault { + color: var(--color-blue); +} + .khoj-result-entry { color: var(--text-muted); margin-left: 2em; @@ -803,4 +811,51 @@ img.copy-icon { .folder-suggest-item { padding: 4px 8px; display: block; +} + +/* Animation de chargement */ +.khoj-loading { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.khoj-loading-spinner { + width: 24px; + height: 24px; + border: 3px solid var(--background-modifier-border); + border-top: 3px solid var(--text-accent); + border-radius: 50%; + animation: khoj-spin 1s linear infinite; +} + +@keyframes khoj-spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* Research Spinner */ +.search-loading-spinner { + width: 24px; + height: 24px; + border: 3px solid var(--background-modifier-border); + border-top: 3px solid var(--text-accent); + border-radius: 50%; + animation: search-spin 0.8s linear infinite; +} + +@keyframes search-spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } \ No newline at end of file From f42b0cb08c425c7d67a7d99e13ac433ffc096853 Mon Sep 17 00:00:00 2001 From: Henri Jamet Date: Wed, 8 Jan 2025 09:31:43 +0100 Subject: [PATCH 5/5] Refactor comments and CSS for improved clarity in Khoj plugin - Translated comments from French to English for better accessibility and understanding. - Updated CSS comment for loading animation to reflect the change in language. - Enhanced code readability by ensuring consistent language usage across multiple files. - Improved user experience by clarifying the purpose of various functions and settings in the codebase. --- src/interface/obsidian/src/main.ts | 12 ++++++------ src/interface/obsidian/src/search_modal.ts | 8 ++++---- src/interface/obsidian/src/settings.ts | 6 +++--- src/interface/obsidian/src/utils.ts | 4 ++-- src/interface/obsidian/styles.css | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/interface/obsidian/src/main.ts b/src/interface/obsidian/src/main.ts index bb4eecbd5..a75d6040c 100644 --- a/src/interface/obsidian/src/main.ts +++ b/src/interface/obsidian/src/main.ts @@ -59,18 +59,18 @@ export default class Khoj extends Plugin { // Add a settings tab so the user can configure khoj this.addSettingTab(new KhojSettingTab(this.app, this)); - // Démarrer le timer de synchronisation + // Start the sync timer this.startSyncTimer(); } - // Méthode pour démarrer le timer de synchronisation + // Method to start the sync timer private startSyncTimer() { - // Nettoyer l'ancien timer s'il existe + // Clean up the old timer if it exists if (this.indexingTimer) { clearInterval(this.indexingTimer); } - // Démarrer un nouveau timer avec l'intervalle configuré + // Start a new timer with the configured interval this.indexingTimer = setInterval(async () => { if (this.settings.autoConfigure) { this.settings.lastSync = await updateContentIndex( @@ -79,10 +79,10 @@ export default class Khoj extends Plugin { this.settings.lastSync ); } - }, this.settings.syncInterval * 60 * 1000); // Convertir les minutes en millisecondes + }, this.settings.syncInterval * 60 * 1000); // Convert minutes to milliseconds } - // Méthode publique pour redémarrer le timer (appelée depuis les paramètres) + // Public method to restart the timer (called from settings) public restartSyncTimer() { this.startSyncTimer(); } diff --git a/src/interface/obsidian/src/search_modal.ts b/src/interface/obsidian/src/search_modal.ts index 0e1093ed9..fdf727016 100644 --- a/src/interface/obsidian/src/search_modal.ts +++ b/src/interface/obsidian/src/search_modal.ts @@ -13,7 +13,7 @@ export class KhojSearchModal extends SuggestModal { find_similar_notes: boolean; query: string = ""; app: App; - currentController: AbortController | null = null; // Pour annuler les requêtes + currentController: AbortController | null = null; // To cancel requests isLoading: boolean = false; loadingEl: HTMLElement; @@ -37,11 +37,11 @@ export class KhojSearchModal extends SuggestModal { this.loadingEl.style.zIndex = "1000"; this.loadingEl.style.display = "none"; - // Ajouter l'élément au modal + // Add the element to the modal this.modalEl.appendChild(this.loadingEl); // Customize empty state message - // @ts-ignore - Accès à la propriété privée pour personnaliser le message + // @ts-ignore - Access to private property to customize the message this.emptyStateText = ""; // Register Modal Keybindings to Rerank Results @@ -99,7 +99,7 @@ export class KhojSearchModal extends SuggestModal { } async getSuggestions(query: string): Promise { - // Ne pas afficher le chargement si la requête est vide + // Do not show loading if the query is empty if (!query.trim()) { this.isLoading = false; this.updateLoadingState(); diff --git a/src/interface/obsidian/src/settings.ts b/src/interface/obsidian/src/settings.ts index 54dbb812b..15bc5b01c 100644 --- a/src/interface/obsidian/src/settings.ts +++ b/src/interface/obsidian/src/settings.ts @@ -178,7 +178,7 @@ export class KhojSettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.syncInterval = parseInt(value); await this.plugin.saveSettings(); - // Redémarrer le timer avec le nouvel intervalle + // Restart the timer with the new interval this.plugin.restartSyncTimer(); })); @@ -311,13 +311,13 @@ class FolderSuggestModal extends SuggestModal { const folders = new Set(); folders.add(''); // Root folder - // Récupérer tous les fichiers et extraire les chemins des dossiers + // Get all files and extract folder paths this.app.vault.getAllLoadedFiles().forEach(file => { const folderPath = file.parent?.path; if (folderPath) { folders.add(folderPath); - // Ajouter aussi tous les dossiers parents + // Also add all parent folders let parent = folderPath; while (parent.includes('/')) { parent = parent.substring(0, parent.lastIndexOf('/')); diff --git a/src/interface/obsidian/src/utils.ts b/src/interface/obsidian/src/utils.ts index b0dbf61f6..27903af4c 100644 --- a/src/interface/obsidian/src/utils.ts +++ b/src/interface/obsidian/src/utils.ts @@ -74,9 +74,9 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las }) // Filter files based on specified folders .filter(file => { - // Si aucun dossier n'est spécifié, synchroniser tous les fichiers + // If no folders are specified, sync all files if (setting.syncFolders.length === 0) return true; - // Sinon, vérifier si le fichier est dans un des dossiers spécifiés + // Otherwise, check if the file is in one of the specified folders return setting.syncFolders.some(folder => file.path.startsWith(folder + '/') || file.path === folder ); diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 728c064f9..23113c906 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -813,7 +813,7 @@ img.copy-icon { display: block; } -/* Animation de chargement */ +/* Loading animation */ .khoj-loading { display: flex; justify-content: center;