refactor: convert TypeScript namespaces to standalone functions

Migrate AIProvider and SearchTrigger from namespace pattern to exported functions. Simplify OpenAITokenService async implementation, remove unused imports, fix enum aliases, and update placeholder text.
This commit is contained in:
Andrew Beal 2025-11-10 12:59:48 +00:00
parent 96a90452f8
commit 643129517a
12 changed files with 108 additions and 113 deletions

View file

@ -3,12 +3,12 @@ import { countTokens } from 'gpt-tokenizer'
export class OpenAITokenService implements ITokenService {
public async countTokens(input: string): Promise<number> {
public countTokens(input: string): Promise<number> {
if (input.trim() === "") {
return 0;
return Promise.resolve(0);
}
return countTokens(input);
return Promise.resolve(countTokens(input));
}
}

View file

@ -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);

View file

@ -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 (

View file

@ -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 {

View file

@ -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",

View file

@ -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<HTMLService>(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<HTMLService>(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;
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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<SettingsService>(Services.SettingsService);
const provider = AIProvider.fromModel(settingsService.settings.model);
const provider = fromModel(settingsService.settings.model);
if (provider == AIProvider.Claude) {
RegisterSingleton<IAIClass>(Services.IAIClass, new Claude());

View file

@ -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);
}

View file

@ -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();

View file

@ -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,