diff --git a/AIClasses/OpenAI/OpenAITokenService.ts b/AIClasses/OpenAI/OpenAITokenService.ts index 2575c21..47bcf38 100644 --- a/AIClasses/OpenAI/OpenAITokenService.ts +++ b/AIClasses/OpenAI/OpenAITokenService.ts @@ -3,12 +3,12 @@ import { countTokens } from 'gpt-tokenizer' export class OpenAITokenService implements ITokenService { - public async countTokens(input: string): Promise { + public countTokens(input: string): Promise { if (input.trim() === "") { - return 0; + return Promise.resolve(0); } - return countTokens(input); + return Promise.resolve(countTokens(input)); } } \ No newline at end of file diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index 87a911e..48c9e8c 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -5,7 +5,7 @@ import type { ISearchState, SearchStateStore } from "Stores/SearchStateStore"; import { Resolve } from "Services/DependencyService"; import { Services } from "Services/Services"; - import { SearchTrigger } from "Enums/SearchTrigger"; + import { SearchTrigger, isSearchTrigger, isSearchTriggerElement, fromInput, toNode, triggerToText } from "Enums/SearchTrigger"; import ChatSearchResults from "./ChatSearchResults.svelte"; import type { Writable } from "svelte/store"; import type { InputService } from "Services/InputService"; @@ -60,7 +60,7 @@ } const request = textareaElement.innerHTML; - const formattedRequest = SearchTrigger.triggerToText(request); + const formattedRequest = triggerToText(request); textareaElement.textContent = ""; userRequest = ""; @@ -89,7 +89,7 @@ e.preventDefault(); const elementBeforeCursor = inputService.getElementBeforeCursor(textareaElement); - if (elementBeforeCursor && SearchTrigger.isSearchTriggerElement(elementBeforeCursor)) { + if (elementBeforeCursor && isSearchTriggerElement(elementBeforeCursor)) { elementBeforeCursor.remove(); return; } @@ -107,11 +107,11 @@ handleSubmit(); } - if (SearchTrigger.isSearchTrigger(e.key)) { + if (isSearchTrigger(e.key)) { e.preventDefault(); const position = inputService.getCursorPosition(textareaElement); - const trigger = SearchTrigger.fromInput(e.key); + const trigger = fromInput(e.key); searchStateStore.initializeSearch(trigger, position); @@ -162,7 +162,7 @@ function handleSearchResultAcceptance() { if ($searchState.selectedResult !== "" && $searchState.position != null && $searchState.trigger != null) { - const node = SearchTrigger.toNode($searchState.trigger, $searchState.selectedResult); + const node = toNode($searchState.trigger, $searchState.selectedResult); inputService.deleteTextRange($searchState.position, inputService.getCursorPosition(textareaElement), textareaElement); inputService.insertElementAtCursor(node, textareaElement); diff --git a/Conversations/ConversationContent.ts b/Conversations/ConversationContent.ts index a94cfdd..650c4f5 100644 --- a/Conversations/ConversationContent.ts +++ b/Conversations/ConversationContent.ts @@ -21,7 +21,7 @@ export class ConversationContent { this.toolId = toolId; } - public static isConversationContentData(data: unknown): data is { + public static isConversationContentData(this: void, data: unknown): data is { role: string; content: string; promptContent: string; functionCall: string; timestamp: string, isFunctionCall: boolean, isFunctionCallResponse: boolean, toolId?: string } { return ( diff --git a/Enums/ApiProvider.ts b/Enums/ApiProvider.ts index a7bda2f..ef8de83 100644 --- a/Enums/ApiProvider.ts +++ b/Enums/ApiProvider.ts @@ -4,17 +4,15 @@ export enum AIProvider { OpenAI = "OpenAI" } -export namespace AIProvider { - export function fromModel(model: string): AIProvider { - if (model.startsWith("claude-")) { - return AIProvider.Claude; - } else if (model.startsWith("gemini-")) { - return AIProvider.Gemini; - } else if (model.startsWith("gpt-")) { - return AIProvider.OpenAI; - } else { - throw new Error("Invalid Model Selection!"); - } +export function fromModel(model: string): AIProvider { + if (model.startsWith("claude-")) { + return AIProvider.Claude; + } else if (model.startsWith("gemini-")) { + return AIProvider.Gemini; + } else if (model.startsWith("gpt-")) { + return AIProvider.OpenAI; + } else { + throw new Error("Invalid Model Selection!"); } } @@ -43,10 +41,10 @@ export enum AIProviderModel { GPT_4_1_Mini = "gpt-4.1-mini", GPT_4_1_Nano = "gpt-4.1-nano", - // Conversation naming models - ClaudeNamer = "claude-haiku-4-5-20251001", - GeminiNamer = "gemini-2.5-flash-lite", - OpenAINamer = "gpt-4o-mini", + // Conversation naming models (aliases to existing models) + ClaudeNamer = ClaudeHaiku_4_5, + GeminiNamer = GeminiFlash_2_5_Lite, + OpenAINamer = GPT_4o_Mini, } export enum AIProviderURL { diff --git a/Enums/Copy.ts b/Enums/Copy.ts index 3c6d410..52e37b4 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -50,7 +50,7 @@ export enum Copy { // Settings Placeholders PlaceholderEnterApiKey = "Enter your API key", - PlaceholderFileExclusions = "Examples:\n\nprivate/**\n*.secret.md\njournal/personal/**\n.obsidian/workspace.json", + PlaceholderFileExclusions = "Examples:\n\nprivate/**\n*.secret.md\njournal/personal/**", // Settings Tooltips TooltipShowApiKey = "Show API Key", diff --git a/Enums/SearchTrigger.ts b/Enums/SearchTrigger.ts index c3e110e..d83f80d 100644 --- a/Enums/SearchTrigger.ts +++ b/Enums/SearchTrigger.ts @@ -10,98 +10,95 @@ export enum SearchTrigger { Folder = "/" } -export namespace SearchTrigger { - - const values: string[] = [ - SearchTrigger.Tag, - SearchTrigger.File, - SearchTrigger.Folder - ]; +const values: string[] = [ + SearchTrigger.Tag, + SearchTrigger.File, + SearchTrigger.Folder +]; - export function isSearchTrigger(input: string): boolean { - return values.includes(input); +export function isSearchTrigger(input: string): boolean { + return values.includes(input); +} + +export function isSearchTriggerElement(node: Node): boolean { + return node.nodeType === Node.ELEMENT_NODE && + (node as HTMLElement).tagName === 'SPAN' && + (node as HTMLElement).classList?.contains('search-trigger'); +} + +export function fromInput(input: string): SearchTrigger { + switch(input) { + case "#": + return SearchTrigger.Tag; + case "@": + return SearchTrigger.File; + case "/": + return SearchTrigger.Folder; + default: + throw new Error(`Unknown search trigger: ${input}`); + } +} + +export function toNode(trigger: SearchTrigger, content: string): Node { + let text: string; + + switch (trigger) { + case SearchTrigger.Tag: + text = content; + break; + case SearchTrigger.File: + text = basename(content, extname(content)); + break; + case SearchTrigger.Folder: + text = basename(content) + "/"; + break; } - export function isSearchTriggerElement(node: Node): boolean { - return node.nodeType === Node.ELEMENT_NODE && - (node as HTMLElement).tagName === 'SPAN' && - (node as HTMLElement).classList?.contains('search-trigger'); - } - - export function fromInput(input: string): SearchTrigger { - switch(input) { - case SearchTrigger.Tag: - return SearchTrigger.Tag; - case SearchTrigger.File: - return SearchTrigger.File; - case SearchTrigger.Folder: - return SearchTrigger.Folder; - default: - throw new Error(`Unknown search trigger: ${input}`); + const node = createEl("span", { + text: text, + cls: "search-trigger", + attr: { + contenteditable: false } - } + }); - export function toNode(trigger: SearchTrigger, content: string): Node { - let text: string; + node.dataset.trigger = trigger; + node.dataset.content = content; - switch (trigger) { - case SearchTrigger.Tag: - text = content; - break; - case SearchTrigger.File: - text = basename(content, extname(content)); - break; - case SearchTrigger.Folder: - text = basename(content) + "/"; - break; - } + setTooltip(node, content, { placement: "top" }); - const node = createEl("span", { - text: text, - cls: "search-trigger", - attr: { - contenteditable: false - } - }); + return node; +} - node.dataset.trigger = trigger; - node.dataset.content = content; +export function triggerToText(input: string): string { + const htmlService: HTMLService = Resolve(Services.HTMLService); - setTooltip(node, content, { placement: "top" }); + const temp = htmlService.parseHTMLToContainer(input); + let result = ""; - return node; - } - - export function triggerToText(input: string): string { - const htmlService: HTMLService = Resolve(Services.HTMLService); - - const temp = htmlService.parseHTMLToContainer(input); - let result = ""; - - temp.childNodes.forEach(node => { - if (node.nodeType === Node.TEXT_NODE) { + temp.childNodes.forEach(node => { + if (node.nodeType === Node.TEXT_NODE) { result += node.textContent || ""; - } else if (SearchTrigger.isSearchTriggerElement(node)) { + } else if (isSearchTriggerElement(node)) { const element = node as HTMLElement; const trigger = element.dataset.trigger; const content = element.dataset.content; if (trigger && content) { - switch (trigger) { - case SearchTrigger.Tag: - result += `tag:"${content}"`; - break; - case SearchTrigger.File: - result += `file:"${content}"`; - break; - case SearchTrigger.Folder: - result += `folder:"${content}"`; - break; - } + switch (trigger) { + case "#": + result += `tag:"${content}"`; + break; + case "@": + result += `file:"${content}"`; + break; + case "/": + result += `folder:"${content}"`; + break; + } } - } - }); + } + }); - return result; - } + return result; } \ No newline at end of file diff --git a/Modals/ConversationHistoryModal.ts b/Modals/ConversationHistoryModal.ts index adc0d17..a06a8db 100644 --- a/Modals/ConversationHistoryModal.ts +++ b/Modals/ConversationHistoryModal.ts @@ -1,4 +1,4 @@ -import { App, Modal } from 'obsidian'; +import { Modal } from 'obsidian'; import ConversationHistoryModalSvelte from './ConversationHistoryModalSvelte.svelte'; import type { Conversation } from 'Conversations/Conversation'; import { mount, unmount } from 'svelte'; diff --git a/Services/InputService.ts b/Services/InputService.ts index 8aa2425..d2d304b 100644 --- a/Services/InputService.ts +++ b/Services/InputService.ts @@ -1,4 +1,4 @@ -import { SearchTrigger } from "../Enums/SearchTrigger"; +import { isSearchTriggerElement } from "../Enums/SearchTrigger"; export class InputService { @@ -28,7 +28,7 @@ export class InputService { if (node.nodeType === Node.ELEMENT_NODE) { const el = node as HTMLElement; - if (SearchTrigger.isSearchTriggerElement(node)) { + if (isSearchTriggerElement(node)) { return false; } diff --git a/Services/ServiceRegistration.ts b/Services/ServiceRegistration.ts index 93f2bc7..0192c0a 100644 --- a/Services/ServiceRegistration.ts +++ b/Services/ServiceRegistration.ts @@ -1,4 +1,4 @@ -import { AIProvider } from "Enums/ApiProvider"; +import { AIProvider, fromModel } from "Enums/ApiProvider"; import type VaultkeeperAIPlugin from "main"; import { RegisterSingleton, RegisterTransient, Resolve } from "./DependencyService"; import { Services } from "./Services"; @@ -73,7 +73,7 @@ export function RegisterDependencies() { export function RegisterAiProvider() { const settingsService = Resolve(Services.SettingsService); - const provider = AIProvider.fromModel(settingsService.settings.model); + const provider = fromModel(settingsService.settings.model); if (provider == AIProvider.Claude) { RegisterSingleton(Services.IAIClass, new Claude()); diff --git a/Services/SettingsService.ts b/Services/SettingsService.ts index c458bd6..a8c0272 100644 --- a/Services/SettingsService.ts +++ b/Services/SettingsService.ts @@ -1,7 +1,7 @@ import type VaultkeeperAIPlugin from "main"; import { Resolve } from "./DependencyService"; import { Services } from "./Services"; -import { AIProvider, AIProviderModel } from "Enums/ApiProvider"; +import { AIProvider, AIProviderModel, fromModel } from "Enums/ApiProvider"; const DEFAULT_SETTINGS: IVaultkeeperAISettings = { firstTimeStart: true, @@ -54,7 +54,7 @@ export class SettingsService { } public getApiKeyForCurrentModel(): string { - const provider = AIProvider.fromModel(this.settings.model); + const provider = fromModel(this.settings.model); return this.getApiKeyForProvider(provider); } diff --git a/VaultkeeperAISettingTab.ts b/VaultkeeperAISettingTab.ts index 5fb0991..5446f18 100644 --- a/VaultkeeperAISettingTab.ts +++ b/VaultkeeperAISettingTab.ts @@ -1,4 +1,4 @@ -import { AIProvider, AIProviderModel } from "Enums/ApiProvider"; +import { AIProviderModel, fromModel } from "Enums/ApiProvider"; import { Copy } from "Enums/Copy"; import { Selector } from "Enums/Selector"; import type VaultkeeperAIPlugin from "main"; @@ -132,7 +132,7 @@ export class VaultkeeperAISettingTab extends PluginSettingTab { text.setPlaceholder(Copy.PlaceholderEnterApiKey) .setValue(this.settingsService.getApiKeyForCurrentModel()) .onChange(async (value) => { - const provider = AIProvider.fromModel(this.settingsService.settings.model); + const provider = fromModel(this.settingsService.settings.model); this.settingsService.setApiKeyForProvider(provider, value); await this.settingsService.saveSettings(() => RegisterAiProvider()); this.highlightApiKey(); diff --git a/__tests__/Services/SettingsService.test.ts b/__tests__/Services/SettingsService.test.ts index f4aa018..3a3871a 100644 --- a/__tests__/Services/SettingsService.test.ts +++ b/__tests__/Services/SettingsService.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { SettingsService, IVaultkeeperAISettings } from '../../Services/SettingsService'; import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService'; import { Services } from '../../Services/Services'; -import { AIProvider, AIProviderModel } from '../../Enums/ApiProvider'; +import { AIProvider, AIProviderModel, fromModel } from '../../Enums/ApiProvider'; describe('SettingsService', () => { let settingsService: SettingsService; @@ -185,7 +185,7 @@ describe('SettingsService', () => { expect(key).toBe('gemini-key'); }); - it('should use AIProvider.fromModel to determine provider', () => { + it('should use fromModel to determine provider', () => { // Test with various Claude models settingsService = new SettingsService({ model: AIProviderModel.ClaudeOpus_4,