mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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:
parent
96a90452f8
commit
643129517a
12 changed files with 108 additions and 113 deletions
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue