mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Remove version numbers from Mistral model enum names (MistralLarge_3 → MistralLarge, MistralMedium_3_1 → MistralMedium, MistralSmall_3_2 → MistralSmall) and update display text to indicate "latest" versions. This aligns naming with the underlying "latest" API endpoints and improves maintainability.
275 lines
No EOL
11 KiB
TypeScript
275 lines
No EOL
11 KiB
TypeScript
import { AIProvider, AIProviderModel, fromModel } from "Enums/ApiProvider";
|
|
import { Copy } from "Enums/Copy";
|
|
import { Selector } from "Enums/Selector";
|
|
import type VaultkeeperAIPlugin from "main";
|
|
import { HelpModal } from "Modals/HelpModal";
|
|
import { DropdownComponent, PluginSettingTab, Setting, setIcon, setTooltip } from "obsidian";
|
|
import { Resolve } from "Services/DependencyService";
|
|
import type { SettingsService } from "Services/SettingsService";
|
|
import { Services } from "Services/Services";
|
|
import { RegisterAiProvider } from "Services/ServiceRegistration";
|
|
|
|
export class VaultkeeperAISettingTab extends PluginSettingTab {
|
|
private readonly settingsService: SettingsService;
|
|
|
|
private apiKeySetting: Setting | null = null;
|
|
private apiKeyInputEl: HTMLInputElement | null = null;
|
|
private fileDisclaimerSetting: Setting | null = null;
|
|
private planningModelDropdown: DropdownComponent | null = null;
|
|
|
|
constructor() {
|
|
const plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
|
super(plugin.app, plugin);
|
|
this.settingsService = Resolve<SettingsService>(Services.SettingsService);
|
|
}
|
|
|
|
public display() {
|
|
const { containerEl } = this;
|
|
|
|
containerEl.empty();
|
|
|
|
/* Model Selection Setting */
|
|
new Setting(containerEl)
|
|
.setName(Copy.SettingModel)
|
|
.setDesc(Copy.SettingModelDesc)
|
|
.addDropdown((dropdown) => {
|
|
this.populateModelDropdown(dropdown);
|
|
dropdown.setValue(this.settingsService.settings.model);
|
|
dropdown.onChange(async (value) => {
|
|
this.settingsService.settings.model = value;
|
|
await this.settingsService.saveSettings(() => RegisterAiProvider());
|
|
if (this.apiKeyInputEl) {
|
|
this.apiKeyInputEl.value = this.settingsService.getApiKeyForCurrentModel();
|
|
this.highlightApiKey();
|
|
}
|
|
this.updateFileDisclaimer();
|
|
await this.updatePlanningModelDropdown();
|
|
});
|
|
});
|
|
|
|
/* Planning Model Selection Setting */
|
|
const currentProvider = fromModel(this.settingsService.settings.model);
|
|
const planningModelDescFragment = document.createDocumentFragment();
|
|
planningModelDescFragment.appendText(Copy.SettingPlanningModelDesc);
|
|
planningModelDescFragment.createEl("br");
|
|
planningModelDescFragment.createEl("br");
|
|
planningModelDescFragment.createEl("span", { text: Copy.SettingPlanningModelTip, cls: "planning-model-description-tip" });
|
|
new Setting(containerEl)
|
|
.setName(Copy.SettingPlanningModel)
|
|
.setDesc(planningModelDescFragment)
|
|
.addDropdown((dropdown) => {
|
|
this.planningModelDropdown = dropdown;
|
|
this.populateModelDropdown(dropdown, currentProvider);
|
|
dropdown.setValue(this.settingsService.settings.planningModel);
|
|
dropdown.onChange(async (value) => {
|
|
this.settingsService.settings.planningModel = value;
|
|
await this.settingsService.saveSettings();
|
|
});
|
|
});
|
|
|
|
/* API Key Setting */
|
|
this.apiKeySetting = new Setting(containerEl)
|
|
.setName(Copy.SettingApiKey)
|
|
.setDesc(Copy.SettingApiKeyDesc)
|
|
.addText(text => {
|
|
text.setPlaceholder(Copy.PlaceholderEnterApiKey)
|
|
.setValue(this.settingsService.getApiKeyForCurrentModel())
|
|
.onChange(async (value) => {
|
|
const provider = fromModel(this.settingsService.settings.model);
|
|
this.settingsService.setApiKeyForProvider(provider, value);
|
|
await this.settingsService.saveSettings(() => RegisterAiProvider());
|
|
this.highlightApiKey();
|
|
});
|
|
text.inputEl.type = "password";
|
|
this.apiKeyInputEl = text.inputEl;
|
|
})
|
|
.addExtraButton(button => {
|
|
button
|
|
.setTooltip(Copy.TooltipShowApiKey)
|
|
.onClick(() => {
|
|
if (this.apiKeyInputEl && this.apiKeyInputEl.type === "password") {
|
|
this.apiKeyInputEl.type = "text";
|
|
setIcon(button.extraSettingsEl, "eye-off");
|
|
setTooltip(button.extraSettingsEl, Copy.TooltipHideApiKey);
|
|
} else if (this.apiKeyInputEl) {
|
|
this.apiKeyInputEl.type = "password";
|
|
setIcon(button.extraSettingsEl, "eye");
|
|
setTooltip(button.extraSettingsEl, Copy.TooltipShowApiKey);
|
|
}
|
|
});
|
|
setIcon(button.extraSettingsEl, "eye");
|
|
});
|
|
this.highlightApiKey();
|
|
|
|
/* Exclusions Setting */
|
|
new Setting(containerEl)
|
|
.setName(Copy.SettingFileExclusions)
|
|
.setDesc(Copy.SettingFileExclusionsDesc)
|
|
.addTextArea(text => {
|
|
text.setPlaceholder(Copy.PlaceholderFileExclusions)
|
|
.setValue(this.settingsService.settings.exclusions.join("\n"))
|
|
.onChange(async (value) => {
|
|
this.settingsService.settings.exclusions = value.split("\n").map(line => line.trim()).filter(line => line.length > 0);
|
|
await this.settingsService.saveSettings();
|
|
});
|
|
text.inputEl.classList.add(Selector.AIExclusionsInput);
|
|
});
|
|
|
|
/* Context Header */
|
|
new Setting(containerEl)
|
|
.setHeading()
|
|
.setName(Copy.SettingContext);
|
|
|
|
/* Search Results Limit Setting */
|
|
new Setting(containerEl)
|
|
.setName(Copy.SettingSearchResultsLimit)
|
|
.setDesc(Copy.SettingSearchResultsLimitDesc)
|
|
.addSlider(slider => {
|
|
slider
|
|
.setLimits(5, 40, 1)
|
|
.setValue(this.settingsService.settings.searchResultsLimit)
|
|
.setDynamicTooltip()
|
|
.onChange(async (value) => {
|
|
this.settingsService.settings.searchResultsLimit = value;
|
|
await this.settingsService.saveSettings();
|
|
});
|
|
});
|
|
|
|
/* Snippet Size Limit Setting */
|
|
new Setting(containerEl)
|
|
.setName(Copy.SettingSnippetSizeLimit)
|
|
.setDesc(Copy.SettingSnippetSizeLimitDesc)
|
|
.addSlider(slider => {
|
|
slider
|
|
.setLimits(50, 1000, 10)
|
|
.setValue(this.settingsService.settings.snippetSizeLimit)
|
|
.setDynamicTooltip()
|
|
.onChange(async (value) => {
|
|
this.settingsService.settings.snippetSizeLimit = value;
|
|
await this.settingsService.saveSettings();
|
|
});
|
|
});
|
|
|
|
/* File Monitoring Guidelines */
|
|
new Setting(containerEl)
|
|
.setHeading()
|
|
.setName(Copy.SettingFileMonitoringHeading);
|
|
|
|
this.fileDisclaimerSetting = new Setting(containerEl)
|
|
.setDesc(Copy.SettingFileMonitoringClaude)
|
|
.addExtraButton(button => {
|
|
button
|
|
.setTooltip(Copy.TooltipLearnMoreFileMonitoring)
|
|
.onClick(() => {
|
|
const modal = Resolve<HelpModal>(Services.HelpModal);
|
|
modal.open(2); // Opens HelpModal to "Plugin Guide" (topic 2)
|
|
});
|
|
setIcon(button.extraSettingsEl, "help-circle");
|
|
});
|
|
|
|
this.updateFileDisclaimer();
|
|
}
|
|
|
|
private populateModelDropdown(dropdown: DropdownComponent, providerFilter?: AIProvider): void {
|
|
const select = dropdown.selectEl;
|
|
|
|
// Claude models
|
|
if (!providerFilter || providerFilter === AIProvider.Claude) {
|
|
const claudeGroup = select.createEl("optgroup", { attr: { label: Copy.ProviderClaude } });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeSonnet_4_6, text: Copy.ClaudeSonnet_4_6 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeSonnet_4_5, text: Copy.ClaudeSonnet_4_5 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeSonnet_4, text: Copy.ClaudeSonnet_4 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeOpus_4_6, text: Copy.ClaudeOpus_4_6 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeOpus_4_5, text: Copy.ClaudeOpus_4_5 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeOpus_4_1, text: Copy.ClaudeOpus_4_1 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeOpus_4, text: Copy.ClaudeOpus_4 });
|
|
claudeGroup.createEl("option", { value: AIProviderModel.ClaudeHaiku_4_5, text: Copy.ClaudeHaiku_4_5 });
|
|
}
|
|
|
|
// OpenAI models
|
|
if (!providerFilter || providerFilter === AIProvider.OpenAI) {
|
|
const openaiGroup = select.createEl("optgroup", { attr: { label: Copy.ProviderOpenAI } });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_2_Instant, text: Copy.GPT_5_2_Instant });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_2_Thinking, text: Copy.GPT_5_2_Thinking });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_2_Pro, text: Copy.GPT_5_2_Pro });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_1, text: Copy.GPT_5_1 });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5, text: Copy.GPT_5 });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_Mini, text: Copy.GPT_5_Mini });
|
|
openaiGroup.createEl("option", { value: AIProviderModel.GPT_5_Nano, text: Copy.GPT_5_Nano });
|
|
}
|
|
|
|
// Gemini models
|
|
if (!providerFilter || providerFilter === AIProvider.Gemini) {
|
|
const geminiGroup = select.createEl("optgroup", { attr: { label: Copy.ProviderGemini } });
|
|
geminiGroup.createEl("option", { value: AIProviderModel.GeminiFlash_2_5_Lite, text: Copy.GeminiFlash_2_5_Lite });
|
|
geminiGroup.createEl("option", { value: AIProviderModel.GeminiFlash_2_5, text: Copy.GeminiFlash_2_5 });
|
|
geminiGroup.createEl("option", { value: AIProviderModel.GeminiPro_2_5, text: Copy.GeminiPro_2_5 });
|
|
geminiGroup.createEl("option", { value: AIProviderModel.GeminiFlash_3_Preview, text: Copy.GeminiFlash_3_Preview });
|
|
geminiGroup.createEl("option", { value: AIProviderModel.GeminiPro_3_1_Preview, text: Copy.GeminiPro_3_1_Preview });
|
|
}
|
|
|
|
// Mistral models
|
|
if (!providerFilter || providerFilter === AIProvider.Mistral) {
|
|
const mistralGroup = select.createEl("optgroup", { attr: { label: Copy.ProviderMistral } });
|
|
mistralGroup.createEl("option", { value: AIProviderModel.MistralLarge, text: Copy.MistralLarge });
|
|
mistralGroup.createEl("option", { value: AIProviderModel.MistralMedium, text: Copy.MistralMedium });
|
|
mistralGroup.createEl("option", { value: AIProviderModel.MistralSmall, text: Copy.MistralSmall });
|
|
}
|
|
}
|
|
|
|
private async updatePlanningModelDropdown(): Promise<void> {
|
|
if (!this.planningModelDropdown) return;
|
|
|
|
const currentProvider = fromModel(this.settingsService.settings.model);
|
|
const planningProvider = fromModel(this.settingsService.settings.planningModel);
|
|
|
|
// Clear existing options
|
|
this.planningModelDropdown.selectEl.empty();
|
|
this.populateModelDropdown(this.planningModelDropdown, currentProvider);
|
|
|
|
// If planning model provider doesn't match, reset to main model
|
|
if (planningProvider !== currentProvider) {
|
|
this.settingsService.settings.planningModel = this.settingsService.settings.model;
|
|
await this.settingsService.saveSettings();
|
|
}
|
|
|
|
this.planningModelDropdown.setValue(this.settingsService.settings.planningModel);
|
|
}
|
|
|
|
private highlightApiKey() {
|
|
if (this.apiKeySetting) {
|
|
const currentApiKey = this.settingsService.getApiKeyForCurrentModel();
|
|
if (currentApiKey.trim() === "") {
|
|
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingOk);
|
|
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingError);
|
|
} else {
|
|
this.apiKeySetting.settingEl.removeClass(Selector.ApiKeySettingError);
|
|
this.apiKeySetting.settingEl.addClass(Selector.ApiKeySettingOk);
|
|
}
|
|
}
|
|
}
|
|
|
|
private updateFileDisclaimer() {
|
|
if (this.fileDisclaimerSetting) {
|
|
const provider = fromModel(this.settingsService.settings.model);
|
|
let disclaimerText;
|
|
|
|
switch(provider) {
|
|
case AIProvider.Gemini:
|
|
disclaimerText = Copy.SettingFileMonitoringGemini;
|
|
break;
|
|
case AIProvider.Claude:
|
|
disclaimerText = Copy.SettingFileMonitoringClaude;
|
|
break;
|
|
case AIProvider.OpenAI:
|
|
disclaimerText = Copy.SettingFileMonitoringOpenAI;
|
|
break;
|
|
case AIProvider.Mistral:
|
|
disclaimerText = Copy.SettingFileMonitoringMistral;
|
|
break;
|
|
}
|
|
|
|
this.fileDisclaimerSetting.setDesc(disclaimerText);
|
|
}
|
|
}
|
|
} |