Files
obsidian_ollama/src/main.ts
T

232 lines
7.8 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<PluginSettings> | 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<void> {
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 (01) 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();
}
}