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.
This commit is contained in:
Henri Jamet
2024-12-29 13:07:21 +01:00
parent 833ab52986
commit c5c9e0072c
3 changed files with 308 additions and 64 deletions

View File

@@ -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<TFile, number>;
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 = '';
@@ -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<string> {
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<string>();
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();
}
}

View File

@@ -71,6 +71,15 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
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;
@@ -188,8 +197,7 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
return lastSync;
}
export async function openKhojPluginSettings(): Promise<void>
{
export async function openKhojPluginSettings(): Promise<void> {
const setting = this.app.setting;
await setting.open();
setting.openTabById('khoj');

View File

@@ -18,6 +18,7 @@ If your plugin does not need CSS, delete this file.
.khoj-chat p {
margin: 0;
}
.khoj-chat pre {
text-wrap: unset;
}
@@ -33,6 +34,7 @@ If your plugin does not need CSS, delete this file.
font-weight: 300;
line-height: 1.5em;
}
.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,14 +200,17 @@ 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) {
display: block;
}
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);
@@ -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;
}