import { App, Notice, Plugin, TFile } from 'obsidian'; import { PluginSettings, IndexingProgress } from './src/types'; import { DEFAULT_SETTINGS, validateSettings } from './src/settings'; import { IndexingOrchestrator } from './src/indexing/orchestrator'; import { SearchModal } from './src/search/searchModal'; import { QdrantSettingsTab } from './src/ui/settingsTab'; export default class QdrantPlugin extends Plugin { settings: PluginSettings; private indexingOrchestrator: IndexingOrchestrator | null = null; private statusBarItem: HTMLElement | null = null; async onload() { console.log('Qdrant Semantic Search plugin loading...'); try { await this.loadSettings(); console.log('Settings loaded successfully'); // Validate settings const errors = validateSettings(this.settings); if (errors.length > 0) { console.warn('Settings validation warnings:', errors); new Notice('Qdrant: Please configure settings. Settings validation warnings: ' + errors.join(', ')); } // Add status bar item first this.setupStatusBar(); console.log('Status bar added'); // Add commands this.addCommands(); console.log('Commands registered'); // Add settings tab this.addSettingTab(new QdrantSettingsTab(this.app, this)); console.log('Settings tab added'); // Initialize indexing orchestrator (non-blocking) this.initializeOrchestrator(); console.log('Qdrant Semantic Search plugin loaded successfully'); new Notice('Qdrant Semantic Search loaded! Configure settings before indexing.'); } catch (error) { console.error('Failed to load Qdrant plugin:', error); new Notice('Failed to load Qdrant plugin: ' + (error as Error).message); throw error; } } private async initializeOrchestrator() { try { console.log('Initializing indexing orchestrator...'); console.log('Creating orchestrator with Qdrant URL:', this.settings.qdrant.url); this.indexingOrchestrator = new IndexingOrchestrator(this.app, this.settings); await this.indexingOrchestrator.initialize(); // Set up progress tracking this.setupProgressTracking(); console.log('Indexing orchestrator initialized successfully'); this.updateStatusBar('Ready'); } catch (error) { console.error('Failed to initialize indexing orchestrator:', error); console.error('Stack trace:', (error as Error).stack); new Notice('Qdrant: Indexing system not ready. Please check settings and connection.'); this.updateStatusBar('Not configured'); } } onunload() { // Shutdown indexing orchestrator if (this.indexingOrchestrator) { this.indexingOrchestrator.shutdown(); } } /** * Deep merge two objects, recursively merging nested objects */ private deepMerge(target: any, source: any): any { const output = Object.assign({}, target); if (this.isObject(target) && this.isObject(source)) { Object.keys(source).forEach(key => { if (this.isObject(source[key])) { if (!(key in target)) { Object.assign(output, { [key]: source[key] }); } else { output[key] = this.deepMerge(target[key], source[key]); } } else { Object.assign(output, { [key]: source[key] }); } }); } return output; } private isObject(item: any): boolean { return item && typeof item === 'object' && !Array.isArray(item); } async loadSettings() { const loadedData = await this.loadData(); console.log('Loading settings from data.json:', JSON.stringify(loadedData, null, 2)); // Use deep merge to properly combine defaults with saved settings this.settings = this.deepMerge(DEFAULT_SETTINGS, loadedData || {}); console.log('Merged settings (Qdrant URL):', this.settings.qdrant.url); } async saveSettings() { console.log('Saving settings (Qdrant URL):', this.settings.qdrant.url); await this.saveData(this.settings); // Reinitialize orchestrator with new settings console.log('Reinitializing orchestrator with updated settings...'); if (this.indexingOrchestrator) { await this.indexingOrchestrator.shutdown(); this.indexingOrchestrator = null; } // Reinitialize with new settings await this.initializeOrchestrator(); } private setupStatusBar() { this.statusBarItem = this.addStatusBarItem(); this.updateStatusBar('Ready'); } private addCommands() { // Semantic search command this.addCommand({ id: 'semantic-search', name: 'Semantic search', callback: () => { if (!this.indexingOrchestrator?.isReady()) { new Notice('Indexing system not ready. Please check your settings.'); return; } new SearchModal(this.app, this.settings).open(); } }); // Index current file command this.addCommand({ id: 'index-current-file', name: 'Index current file', checkCallback: (checking: boolean) => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile instanceof TFile) { if (!checking) { this.indexFile(activeFile); } return true; } return false; } }); // Full reindex command this.addCommand({ id: 'full-reindex', name: 'Full reindex vault', callback: () => { this.indexFullVault(); } }); // Clear index command this.addCommand({ id: 'clear-index', name: 'Clear index', callback: () => { this.clearIndex(); } }); // Open graph view command if (this.settings.enableGraphView) { this.addCommand({ id: 'open-graph-view', name: 'Open graph view', callback: () => { // TODO: Implement graph view new Notice('Graph view not yet implemented'); } }); } } private setupProgressTracking() { if (!this.indexingOrchestrator) return; this.indexingOrchestrator.setProgressCallback((progress: IndexingProgress) => { this.updateStatusBar(progress); }); this.indexingOrchestrator.setErrorCallback((error: string) => { new Notice('Indexing error: ' + error); }); } private updateStatusBar(progress: IndexingProgress | string) { if (!this.statusBarItem) return; if (typeof progress === 'string') { this.statusBarItem.setText(`Qdrant: ${progress}`); return; } if (progress.isRunning) { const percentage = progress.totalFiles > 0 ? Math.round((progress.processedFiles / progress.totalFiles) * 100) : 0; this.statusBarItem.setText(`Qdrant: Indexing ${percentage}% (${progress.processedFiles}/${progress.totalFiles})`); } else { this.statusBarItem.setText('Qdrant: Ready'); } } // Public methods for settings tab async testQdrantConnection(): Promise { if (!this.indexingOrchestrator) return false; const connections = await this.indexingOrchestrator.testConnections(); return connections.qdrant; } async testOllamaConnection(): Promise { if (!this.indexingOrchestrator) return false; const connections = await this.indexingOrchestrator.testConnections(); return connections.embedding; } async indexFullVault(): Promise { if (!this.indexingOrchestrator?.isReady()) { throw new Error('Indexing system not ready'); } await this.indexingOrchestrator.indexFullVault(); } async indexFile(file: TFile): Promise { if (!this.indexingOrchestrator?.isReady()) { throw new Error('Indexing system not ready'); } await this.indexingOrchestrator.indexFile(file); } async clearIndex(): Promise { if (!this.indexingOrchestrator?.isReady()) { throw new Error('Indexing system not ready'); } await this.indexingOrchestrator.clearIndex(); } async getIndexStats(): Promise { if (!this.indexingOrchestrator?.isReady()) { throw new Error('Indexing system not ready'); } return await this.indexingOrchestrator.getIndexStats(); } }