diff --git a/Enums/Copy.ts b/Enums/Copy.ts index 53ac264..b163c47 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -40,6 +40,7 @@ export enum Copy { SettingLocalUrl = "Local URL", SettingLocalUrlDesc = "Enter the URL for the local server", SettingApiKeyLocalDesc = "Enter your API key if authentication is required - optional.", + SettingExclusionsHeading = "Exclusions", SettingFileExclusions = "File exclusions", SettingContext = "Context", SettingSearchResultsLimit = "Search results limit", @@ -64,14 +65,17 @@ export enum Copy { SettingPlanningModelTip = "Tip: You can reduce cost by using a more powerful model for planning and a cheaper model for the regular agent.", SettingQuickActionModelDesc = "Select the AI model to use for quick actions. A fast, lightweight model is recommended.", SettingLocalQuickActionModelDesc = "For best results a smaller local model with a minimum of an 8K context window is recommended.", + SettingLocalModelTemplateWarning = "Note that some models may have very strict templates that require exact 'user' -> 'agent' turns. VaultKeeper AI may chain multiple tool calls and results, so you may find that some models need their Jinja templates adjusting, see: ", + SettingLocalModelTemplateWarningLinkText = "LM Studio prompt template docs (external link)", SettingApiKeyDesc = "Enter your API key here.", SettingFileExclusionsDesc = "Set which directories and files the AI should ignore. Enter one path per line - supports glob patterns like folder/**, *.md", SettingSearchResultsLimitDesc = "Set the maximum number of results provided to the AI when it searches through files in your vault. Higher values provide more context but increase search time.", SettingSnippetSizeLimitDesc = "Set the character limit of search previews provided to the AI when it searches through files in your vault. Higher values provide more context per result.", - SettingFileMonitoringGemini = "Files uploaded to Gemini are automatically deleted after 48 hours and will be re-uploaded during conversations as needed. No manual cleanup is typically required.", - SettingFileMonitoringClaude = "Files uploaded to Claude remain stored indefinitely. Periodically check the Anthropic Console (https://console.anthropic.com/) to review and remove old files that are no longer needed.", - SettingFileMonitoringOpenAI = "Files uploaded to OpenAI remain stored indefinitely. Periodically check the OpenAI Platform (https://platform.openai.com/) to review and remove old files that are no longer needed.", - SettingFileMonitoringMistral = "Documents uploaded to Mistral are stored on their platform. Images are sent inline and not stored. Periodically check the Mistral Console (https://console.mistral.ai/) to review and remove old files that are no longer needed.", + SettingFileMonitoringGemini = "Files uploaded to Gemini are automatically deleted after 48 hours and will be re-uploaded during conversations as needed. No manual cleanup is typically required. ", + SettingFileMonitoringClaude = "Files uploaded to Claude remain stored indefinitely. Periodically check the Anthropic Console to review and remove old files that are no longer needed. ", + SettingFileMonitoringOpenAI = "Files uploaded to OpenAI remain stored indefinitely. Periodically check the OpenAI Platform to review and remove old files that are no longer needed. ", + SettingFileMonitoringMistral = "Documents uploaded to Mistral are stored on their platform. Images are sent inline and not stored. Periodically check the Mistral Console to review and remove old files that are no longer needed. ", + SettingFileMonitoringLinkText = "See the plugin guide for more information.", SettingAccessMemories = "Memories let the AI retain preferences and context across conversations. You can view and edit them at any time.", // Settings Placeholders @@ -84,7 +88,6 @@ export enum Copy { // Settings Tooltips TooltipShowApiKey = "Show API Key", TooltipHideApiKey = "Hide API Key", - TooltipLearnMoreFileMonitoring = "Learn more in Plugin Guide", TooltipAccessMemories = "View Memories", SettingAdvancedSettings = "Advanced Settings", diff --git a/Enums/SearchTrigger.ts b/Enums/SearchTrigger.ts index 6583568..6fbd60a 100644 --- a/Enums/SearchTrigger.ts +++ b/Enums/SearchTrigger.ts @@ -55,7 +55,7 @@ export function toNode(trigger: SearchTrigger, content: string): Node { break; } - const node = createEl("span", { + const node = createSpan({ text: text, cls: "search-trigger", attr: { diff --git a/Enums/Selector.ts b/Enums/Selector.ts index d9409f7..a178b1b 100644 --- a/Enums/Selector.ts +++ b/Enums/Selector.ts @@ -7,6 +7,10 @@ export enum Selector { ConversationHistoryModal = "conversation-history-modal", HelpModal = "help-modal", ContextSettingItemDescription = "context-setting-item-description", + SettingDescIconGrid = "setting-desc-icon-grid", + TemplateWarningIcon = "template-warning-icon", + FileDisclaimerIcon = "file-disclaimer-icon", + FileDisclaimerLink = "file-disclaimer-link", ErrorSelector = "error-selector" } \ No newline at end of file diff --git a/Helpers/DocumentHelper.ts b/Helpers/DocumentHelper.ts index e246ecf..895b8af 100644 --- a/Helpers/DocumentHelper.ts +++ b/Helpers/DocumentHelper.ts @@ -86,7 +86,7 @@ export async function pdfToImages(arrayBuffer: ArrayBuffer, const page = await pdf.getPage(i); const viewport = page.getViewport({ scale }); - const canvas = activeDocument.createElement('canvas'); + const canvas = createEl('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; const ctx = canvas.getContext('2d'); diff --git a/Helpers/StringTools.ts b/Helpers/StringTools.ts index 475cc66..04a8b17 100644 --- a/Helpers/StringTools.ts +++ b/Helpers/StringTools.ts @@ -117,7 +117,7 @@ export abstract class StringTools { } } - const canvas = activeDocument.createElement("canvas"); + const canvas = createEl("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); diff --git a/Services/HTMLService.ts b/Services/HTMLService.ts index 06c4c66..3c4f09b 100644 --- a/Services/HTMLService.ts +++ b/Services/HTMLService.ts @@ -12,7 +12,7 @@ export class HTMLService { public parseHTMLString(htmlString: string): DocumentFragment { const parser = new DOMParser(); - const fragment = activeDocument.createDocumentFragment(); + const fragment = createFragment(); const doc = parser.parseFromString(htmlString, "text/html"); // Transfer all nodes from the parsed body to the fragment @@ -26,7 +26,7 @@ export class HTMLService { // Creates a temporary container, parses HTML, and returns the container. // Useful for parsing HTML when you need to traverse the resulting DOM structure. public parseHTMLToContainer(htmlString: string): HTMLDivElement { - const container = activeDocument.createElement("div"); + const container = createDiv(); const fragment = this.parseHTMLString(htmlString); container.appendChild(fragment); return container; diff --git a/Services/StreamingMarkdownService.ts b/Services/StreamingMarkdownService.ts index 9025507..c87d789 100644 --- a/Services/StreamingMarkdownService.ts +++ b/Services/StreamingMarkdownService.ts @@ -37,7 +37,7 @@ export class StreamingMarkdownService { if (frozenCandidate.length > state.frozenUpTo) { const newSlice = frozenCandidate.slice(state.frozenUpTo); - const tempDiv = activeDocument.createElement("div"); + const tempDiv = createDiv(); await MarkdownRenderer.render(this.plugin.app, newSlice, tempDiv, "", state.component); while (tempDiv.firstChild) { state.frozenContainer.appendChild(tempDiv.firstChild); @@ -58,8 +58,8 @@ export class StreamingMarkdownService { } container.empty(); - const frozenContainer = activeDocument.createElement("div"); - const liveContainer = activeDocument.createElement("div"); + const frozenContainer = createDiv(); + const liveContainer = createDiv(); container.appendChild(frozenContainer); container.appendChild(liveContainer); diff --git a/Styles/custom_styles.css b/Styles/custom_styles.css index cf9c892..f905d71 100644 --- a/Styles/custom_styles.css +++ b/Styles/custom_styles.css @@ -212,6 +212,36 @@ body:not(.is-tablet) .workspace-drawer.mod-right { color: var(--text-muted); } +.setting-desc-icon-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: var(--size-4-2); + align-items: start; +} + +.template-warning-icon { + display: flex; + align-self: anchor-center; + margin: var(--size-4-1); + color: var(--text-warning); +} + +.file-disclaimer-icon { + display: flex; + align-self: anchor-center; + margin: var(--size-4-1); + color: var(--text-muted); +} + +.file-disclaimer-link { + color: var(--text-accent); + cursor: pointer; +} + +.file-disclaimer-link:hover { + text-decoration: underline; +} + .top-bar-button { margin: var(--size-4-2) 0px var(--size-4-2) 0px; padding: var(--size-4-1) var(--size-4-2) var(--size-4-1) var(--size-4-2); diff --git a/Views/VaultkeeperAISettingTab.ts b/Views/VaultkeeperAISettingTab.ts index 7fa5f2c..4788390 100644 --- a/Views/VaultkeeperAISettingTab.ts +++ b/Views/VaultkeeperAISettingTab.ts @@ -79,6 +79,11 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { this.providerSectionEl = containerEl.createDiv(); this.renderProviderSection(); + /* Exclusions Header */ + new Setting(containerEl) + .setHeading() + .setName(Copy.SettingExclusionsHeading); + /* Exclusions Setting */ new Setting(containerEl) .setName(Copy.SettingFileExclusions) @@ -346,6 +351,20 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { }); }); }); + + const templateWarningDescFragment = createFragment(); + const templateWarningGridEl = templateWarningDescFragment.createDiv({ cls: Selector.SettingDescIconGrid }); + setIcon(templateWarningGridEl.createDiv({ cls: Selector.TemplateWarningIcon }), "circle-alert"); + const templateWarningTextEl = templateWarningGridEl.createDiv(); + templateWarningTextEl.appendText(Copy.SettingLocalModelTemplateWarning); + templateWarningTextEl.createEl("a", { + text: Copy.SettingLocalModelTemplateWarningLinkText, + href: "https://lmstudio.ai/docs/app/advanced/prompt-template", + cls: Selector.FileDisclaimerLink + }); + new Setting(containerEl) + .setDesc(templateWarningDescFragment); + } else { /* API Key Setting */ this.apiKeySetting = new Setting(containerEl) @@ -446,17 +465,7 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { }); /* Model files API disclaimer */ - this.fileDisclaimerSetting = new Setting(containerEl) - .setDesc(Copy.SettingFileMonitoringClaude) - .addExtraButton(button => { - button - .setTooltip(Copy.TooltipLearnMoreFileMonitoring) - .onClick(() => { - const modal = Resolve(Services.HelpModal); - modal.open(7); // Opens HelpModal to "Uploaded Files" (topic 7) - }); - setIcon(button.extraSettingsEl, "help-circle"); - }); + this.fileDisclaimerSetting = new Setting(containerEl); this.updateFileDisclaimer(); } } @@ -578,7 +587,7 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { private updateFileDisclaimer() { if (this.fileDisclaimerSetting) { const provider = this.settingsService.settings.provider; - let disclaimerText; + let disclaimerText: string | null; switch(provider) { case AIProvider.Gemini: @@ -594,10 +603,28 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { disclaimerText = Copy.SettingFileMonitoringMistral; break; case AIProvider.Local: - disclaimerText = ""; // Not shown when a local model is being used + disclaimerText = null; // Not shown when a local model is being used } - this.fileDisclaimerSetting.setDesc(disclaimerText); + if (disclaimerText === null) { + this.fileDisclaimerSetting.setDesc(""); + return; + } + + const disclaimerFragment = createFragment(); + const disclaimerGridEl = disclaimerFragment.createDiv({ cls: Selector.SettingDescIconGrid }); + setIcon(disclaimerGridEl.createDiv({ cls: Selector.FileDisclaimerIcon }), "help-circle"); + const disclaimerTextEl = disclaimerGridEl.createDiv(); + disclaimerTextEl.appendText(disclaimerText); + disclaimerTextEl.createEl("a", { + text: Copy.SettingFileMonitoringLinkText, + cls: Selector.FileDisclaimerLink + }).addEventListener("click", () => { + const modal = Resolve(Services.HelpModal); + modal.open(7); // Opens HelpModal to "Uploaded Files" (topic 7) + }); + + this.fileDisclaimerSetting.setDesc(disclaimerFragment); } } } \ No newline at end of file