232 lines
7.8 KiB
TypeScript
Executable File
232 lines
7.8 KiB
TypeScript
Executable File
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 (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();
|
||
}
|
||
}
|