import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian'; import { ChatView } from './chat-view'; import { PluginSettings } from './types'; import { validatePluginSettings, validateOllamaUrl, validateModelName, Logger } from './utils'; import { DEFAULT_SETTINGS } from './constants'; import { ErrorHandler } from './error-handler'; export default class OllamaPlugin extends Plugin { settings: PluginSettings = DEFAULT_SETTINGS; async onload() { // Initialize logging Logger.info('Ollama Plugin loading...', 'plugin'); await this.loadSettings(); Logger.info('Plugin loaded successfully', 'plugin'); try { this.registerView( 'ollama-chat-view', (leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings) ); } catch (error) { Logger.error('Failed to register view: ' + (error as Error).message, 'plugin'); new Notice('Failed to register Ollama chat view'); // Don't throw - let the plugin continue loading other features } try { this.addRibbonIcon('message-square', 'Ollama Chat', async () => { const leaf = this.app.workspace.getLeaf(); await leaf.setViewState({ type: 'ollama-chat-view', active: true, }); await this.app.workspace.revealLeaf(leaf); }); } catch (error) { Logger.error('Failed to add ribbon icon: ' + (error as Error).message, 'plugin'); new Notice('Failed to add Ollama ribbon icon'); // Don't throw - let the plugin continue loading other features } this.addSettingTab(new OllamaSettingTab(this.app, this)); } async loadSettings() { try { const data = (await this.loadData()) as Partial | null; if (data) { Logger.debug('Loading saved settings', 'settings'); this.settings = { ...DEFAULT_SETTINGS, ...data, cacheConfig: { ...DEFAULT_SETTINGS.cacheConfig, ...data.cacheConfig }, }; } } catch (error) { ErrorHandler.handleError(error, 'settings load'); } } async saveSettings() { try { // Validate settings before saving const validationErrors = validatePluginSettings(this.settings); if (validationErrors.length > 0) { Logger.error( 'Validation errors prevented saving settings: ' + validationErrors.join('; '), 'settings' ); new Notice(`Cannot save settings: ${validationErrors[0]}`); return false; } Logger.debug('Saving settings: ' + JSON.stringify(this.settings), 'settings'); await this.saveData(this.settings); Logger.info('Settings saved successfully', 'settings'); return true; } catch (error) { ErrorHandler.handleError(error, 'settings save'); return false; } } // Notify all open ChatView instances when settings change public notifyChatViews(): void { const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view'); leaves.forEach((leaf) => { const view = leaf.view; if (view instanceof ChatView) { view.onSettingsChange(this.settings); } }); } public async clearSemanticCache(): Promise { const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view'); for (const leaf of leaves) { const view = leaf.view; if (view instanceof ChatView) { await view.clearCache(); return; } } } } class OllamaSettingTab extends PluginSettingTab { private plugin: OllamaPlugin; constructor(app: App, plugin: OllamaPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { // Clear any existing content first to prevent duplicates this.containerEl.empty(); // Create container for settings const container = this.containerEl.createDiv() as HTMLElement; new Setting(container) .setName('Ollama URL') .setDesc('URL of your Ollama instance') .addText((text) => text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => { const urlValidation = validateOllamaUrl(value); if (urlValidation.valid) { Logger.debug('URL changed to: ' + value, 'settings'); this.plugin.settings.ollamaUrl = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); } else { Logger.warn('Invalid URL format: ' + urlValidation.error, 'settings'); new Notice(urlValidation.error || 'Invalid Ollama URL format.'); } }) ); new Setting(container) .setName('Model') .setDesc('Model to use for chat') .addText((text) => text.setValue(this.plugin.settings.model).onChange(async (value) => { const modelValidation = validateModelName(value); if (modelValidation.valid) { Logger.debug('Model changed to: ' + value, 'settings'); this.plugin.settings.model = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); } else { Logger.warn('Invalid model name format: ' + modelValidation.error, 'settings'); new Notice(modelValidation.error || 'Invalid model name format.'); } }) ); new Setting(container) .setName('Enable Semantic Cache') .setDesc('Cache responses semantically to speed up repeated queries') .addToggle((toggle) => toggle.setValue(this.plugin.settings.cacheConfig.enabled).onChange(async (value) => { this.plugin.settings.cacheConfig.enabled = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); }) ); new Setting(container) .setName('ChromaDB URL') .setDesc('URL of your ChromaDB instance (used for semantic cache)') .addText((text) => text.setValue(this.plugin.settings.cacheConfig.chromaUrl).onChange(async (value) => { this.plugin.settings.cacheConfig.chromaUrl = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); }) ); new Setting(container) .setName('Cache Embedding Model') .setDesc('Ollama model used to generate embeddings for the semantic cache') .addText((text) => text.setValue(this.plugin.settings.cacheConfig.embeddingModel).onChange(async (value) => { this.plugin.settings.cacheConfig.embeddingModel = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); }) ); new Setting(container) .setName('Cache Similarity Threshold') .setDesc( 'Minimum cosine similarity (0–1) for a cache hit. Higher values require closer matches.' ) .addText((text) => text .setValue(String(this.plugin.settings.cacheConfig.similarityThreshold)) .onChange(async (value) => { const parsed = parseFloat(value); if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) { this.plugin.settings.cacheConfig.similarityThreshold = parsed; await this.plugin.saveSettings(); } else { new Notice('Similarity threshold must be a number between 0 and 1.'); } }) ); new Setting(container) .setName('Clear Semantic Cache') .setDesc('Delete all cached responses from ChromaDB') .addButton((button) => button.setButtonText('Clear Cache').onClick(async () => { try { await this.plugin.clearSemanticCache(); new Notice('Semantic cache cleared.'); } catch { new Notice('Failed to clear semantic cache. Is ChromaDB running?'); } }) ); } hide(): void { // Clear the container to prevent duplicate elements this.containerEl.empty(); } }