Nicholai 68cec8090b Fix critical bugs: settings loading, UUID generation, and chunk metadata
This commit resolves several critical issues that prevented the plugin from
working correctly with Qdrant and adds essential metadata to indexed chunks.

**Settings & Configuration:**
- Fix settings initialization using deep merge instead of shallow Object.assign
  - Prevents nested settings from being lost during load
  - Ensures all default values are properly preserved
- Add orchestrator reinitialization when settings are saved
  - Ensures QdrantClient and embedding providers use updated settings
  - Fixes issue where plugin used localhost instead of saved HTTPS URL

**UUID Generation:**
- Fix generateDeterministicUUID() creating invalid UUIDs
  - Was generating 35-character UUIDs instead of proper 36-character format
  - Now correctly creates valid UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
  - Properly generates segment 5 (12 hex chars) from combined hash data
  - Fixes segment 4 to start with 8/9/a/b per UUID spec
  - Resolves Qdrant API rejections: "value X is not a valid point ID"

**Chunk Metadata:**
- Add chunk_text field to ChunkMetadata type
  - Stores the actual text content of each chunk in Qdrant payload
  - Essential for displaying search results and content preview
- Add model name to chunk metadata
  - Populates model field with embedding provider name (e.g., "nomic-embed-text")
  - Enables tracking which model generated each embedding
  - Supports future multi-model collections

**Debug Logging:**
- Add logging for settings loading and URL tracking
- Add logging for QdrantClient initialization
- Add logging for orchestrator creation with settings

**Documentation:**
- Add CLAUDE.md with comprehensive architecture documentation
  - Build commands and development workflow
  - Core components and data processing pipeline
  - Important implementation details and debugging guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 11:29:48 -06:00

264 lines
7.5 KiB
TypeScript

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<boolean> {
if (!this.indexingOrchestrator) return false;
const connections = await this.indexingOrchestrator.testConnections();
return connections.qdrant;
}
async testOllamaConnection(): Promise<boolean> {
if (!this.indexingOrchestrator) return false;
const connections = await this.indexingOrchestrator.testConnections();
return connections.embedding;
}
async indexFullVault(): Promise<void> {
if (!this.indexingOrchestrator?.isReady()) {
throw new Error('Indexing system not ready');
}
await this.indexingOrchestrator.indexFullVault();
}
async indexFile(file: TFile): Promise<void> {
if (!this.indexingOrchestrator?.isReady()) {
throw new Error('Indexing system not ready');
}
await this.indexingOrchestrator.indexFile(file);
}
async clearIndex(): Promise<void> {
if (!this.indexingOrchestrator?.isReady()) {
throw new Error('Indexing system not ready');
}
await this.indexingOrchestrator.clearIndex();
}
async getIndexStats(): Promise<any> {
if (!this.indexingOrchestrator?.isReady()) {
throw new Error('Indexing system not ready');
}
return await this.indexingOrchestrator.getIndexStats();
}
}