mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Remove Token Services.
The token services weren't providing much value and have become more maintenance than they are worth given the addition of binary conversation data.
This commit is contained in:
parent
d96d4b4b29
commit
385271cb75
13 changed files with 1 additions and 691 deletions
|
|
@ -1,41 +0,0 @@
|
|||
import type { ITokenService } from "AIClasses/ITokenService";
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import { Role } from "Enums/Role";
|
||||
import { AIProvider } from "Enums/ApiProvider";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type { AbortService } from "Services/AbortService";
|
||||
|
||||
export class ClaudeTokenService implements ITokenService {
|
||||
|
||||
private ai: Anthropic;
|
||||
private model: string;
|
||||
private readonly abortService: AbortService;
|
||||
|
||||
public constructor() {
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
this.ai = new Anthropic({
|
||||
apiKey: settingsService.getApiKeyForProvider(AIProvider.Claude),
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
this.model = settingsService.settings.model;
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
}
|
||||
|
||||
public async countTokens(input: string): Promise<number> {
|
||||
if (input.trim() === "") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// to maintain the convenience of the interface we just submit the entire input as one message
|
||||
const result = await this.ai.messages.countTokens({
|
||||
model: this.model,
|
||||
messages: [
|
||||
{ role: Role.User, content: input }
|
||||
]
|
||||
})
|
||||
return result.input_tokens;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import type { ITokenService } from "AIClasses/ITokenService";
|
||||
import { CountTokensResponse, GoogleGenAI } from '@google/genai'
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import { AIProvider } from "Enums/ApiProvider";
|
||||
import type { SettingsService } from "Services/SettingsService";
|
||||
import type { AbortService } from "Services/AbortService";
|
||||
|
||||
export class GeminiTokenService implements ITokenService {
|
||||
|
||||
private readonly ai: GoogleGenAI;
|
||||
private model: string;
|
||||
private readonly abortService: AbortService;
|
||||
|
||||
public constructor() {
|
||||
const settingsService = Resolve<SettingsService>(Services.SettingsService);
|
||||
this.ai = new GoogleGenAI({
|
||||
apiKey: settingsService.getApiKeyForProvider(AIProvider.Gemini)
|
||||
});
|
||||
this.model = settingsService.settings.model;
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
}
|
||||
|
||||
public async countTokens(input: string): Promise<number> {
|
||||
if (input.trim() === "") {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const result: CountTokensResponse = await this.ai.models.countTokens({
|
||||
model: this.model,
|
||||
contents: input
|
||||
});
|
||||
return result.totalTokens ?? -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export interface ITokenService {
|
||||
countTokens(input: string): Promise<number>;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import type { ITokenService } from "AIClasses/ITokenService";
|
||||
import { countTokens } from 'gpt-tokenizer'
|
||||
|
||||
export class OpenAITokenService implements ITokenService {
|
||||
|
||||
public countTokens(input: string): Promise<number> {
|
||||
if (input.trim() === "") {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
return Promise.resolve(countTokens(input));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -49,7 +49,6 @@
|
|||
if (chatContainer) {
|
||||
plugin.registerDomEvent(chatContainer, 'click', handleLinkClick);
|
||||
}
|
||||
chatService.setStatusBarTokens(0, 0);
|
||||
});
|
||||
|
||||
async function handleLinkClick(evt: MouseEvent) {
|
||||
|
|
@ -121,7 +120,6 @@
|
|||
isSubmitting = false;
|
||||
abortService.reset();
|
||||
chatArea.updateChatAreaLayout("smooth", true);
|
||||
await chatService.updateTokenDisplay(conversation);
|
||||
},
|
||||
onCancel: () => {
|
||||
cancelling = true;
|
||||
|
|
@ -131,7 +129,6 @@
|
|||
|
||||
$: if ($conversationStore.shouldReset) {
|
||||
conversation = new Conversation();
|
||||
chatService.setStatusBarTokens(0, 0);
|
||||
|
||||
isSubmitting = false;
|
||||
currentStreamingMessageId = null;
|
||||
|
|
@ -155,7 +152,6 @@
|
|||
conversation = loadedConversation;
|
||||
conversationService.setCurrentConversationPath(filePath);
|
||||
chatService.onNameChanged?.(loadedConversation.title);
|
||||
chatService.updateTokenDisplay(loadedConversation);
|
||||
conversationStore.clearLoadFlag();
|
||||
chatArea.updateChatAreaLayout("instant", true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import type { IAIClass } from "AIClasses/IAIClass";
|
|||
import type { ConversationFileSystemService } from "./ConversationFileSystemService";
|
||||
import type { AIFunctionService } from "./AIFunctionService";
|
||||
import type { ConversationNamingService } from "./ConversationNamingService";
|
||||
import type { ITokenService } from "AIClasses/ITokenService";
|
||||
import type { IPrompt } from "AIClasses/IPrompt";
|
||||
import type { StatusBarService } from "./StatusBarService";
|
||||
import { Conversation } from "Conversations/Conversation";
|
||||
import { ConversationContent } from "Conversations/ConversationContent";
|
||||
import { Role } from "Enums/Role";
|
||||
|
|
@ -32,9 +29,6 @@ export class ChatService {
|
|||
private conversationService: ConversationFileSystemService;
|
||||
private aiFunctionService: AIFunctionService;
|
||||
private namingService: ConversationNamingService;
|
||||
private tokenService: ITokenService | undefined;
|
||||
private prompt: IPrompt;
|
||||
private statusBarService: StatusBarService;
|
||||
private eventService: EventService;
|
||||
private abortService: AbortService;
|
||||
|
||||
|
|
@ -45,8 +39,6 @@ export class ChatService {
|
|||
this.conversationService = Resolve<ConversationFileSystemService>(Services.ConversationFileSystemService);
|
||||
this.aiFunctionService = Resolve<AIFunctionService>(Services.AIFunctionService);
|
||||
this.namingService = Resolve<ConversationNamingService>(Services.ConversationNamingService);
|
||||
this.prompt = Resolve<IPrompt>(Services.IPrompt);
|
||||
this.statusBarService = Resolve<StatusBarService>(Services.StatusBarService);
|
||||
this.eventService = Resolve<EventService>(Services.EventService);
|
||||
this.abortService = Resolve<AbortService>(Services.AbortService);
|
||||
this.semaphore = new Semaphore(1, false);
|
||||
|
|
@ -56,7 +48,6 @@ export class ChatService {
|
|||
|
||||
public resolveAIProvider() {
|
||||
this.ai = Resolve<IAIClass>(Services.IAIClass);
|
||||
this.tokenService = Resolve<ITokenService>(Services.ITokenService);
|
||||
}
|
||||
|
||||
public async submit(conversation: Conversation, allowDestructiveActions: boolean, userRequest: string, formattedRequest: string, callbacks: IChatServiceCallbacks) {
|
||||
|
|
@ -131,35 +122,6 @@ export class ChatService {
|
|||
this.eventService.trigger(Event.DiffClosed);
|
||||
}
|
||||
|
||||
public async updateTokenDisplay(conversation: Conversation) {
|
||||
if (this.tokenService === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const systemInstruction = this.prompt.systemInstruction();
|
||||
const userInstruction = await this.prompt.userInstruction();
|
||||
|
||||
const inputMessages = conversation.contents
|
||||
.filter(message => message.role === Role.User && !message.isFunctionCallResponse)
|
||||
.map(message => message.promptContent)
|
||||
.join("\n");
|
||||
|
||||
const outputMessages = conversation.contents
|
||||
.filter(message => message.role === Role.Assistant && !message.isFunctionCall)
|
||||
.map(message => message.content)
|
||||
.join("\n");
|
||||
|
||||
const inputText = systemInstruction + "\n" + userInstruction + "\n" + inputMessages;
|
||||
const inputTokens = await this.tokenService.countTokens(inputText);
|
||||
const outputTokens = await this.tokenService.countTokens(outputMessages);
|
||||
|
||||
this.setStatusBarTokens(inputTokens, outputTokens);
|
||||
}
|
||||
|
||||
public setStatusBarTokens(inputTokens: number, outputTokens: number) {
|
||||
this.statusBarService.animateTokens(inputTokens, outputTokens);
|
||||
}
|
||||
|
||||
private async saveConversation(conversation: Conversation) {
|
||||
const result = await this.conversationService.saveConversation(conversation);
|
||||
if (result instanceof Error) {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ import { WorkSpaceService } from "./WorkSpaceService";
|
|||
import { ChatService } from "./ChatService";
|
||||
import { ConversationNamingService } from "./ConversationNamingService";
|
||||
import { VaultService } from "./VaultService";
|
||||
import type { ITokenService } from "AIClasses/ITokenService";
|
||||
import { GeminiTokenService } from "AIClasses/Gemini/GeminiTokenService";
|
||||
import { StatusBarService } from "./StatusBarService";
|
||||
import { ClaudeTokenService } from "AIClasses/Claude/ClaudeTokenService";
|
||||
import { OpenAITokenService } from "AIClasses/OpenAI/OpenAITokenService";
|
||||
import { ClaudeConversationNamingService } from "AIClasses/Claude/ClaudeConversationNamingService";
|
||||
import { Claude } from "AIClasses/Claude/Claude";
|
||||
import { OpenAIConversationNamingService } from "AIClasses/OpenAI/OpenAIConversationNamingService";
|
||||
|
|
@ -47,7 +42,6 @@ export async function RegisterPlugin(plugin: VaultkeeperAIPlugin) {
|
|||
export function RegisterDependencies() {
|
||||
RegisterSingleton<EventService>(Services.EventService, new EventService());
|
||||
RegisterSingleton<AbortService>(Services.AbortService, new AbortService());
|
||||
RegisterSingleton<StatusBarService>(Services.StatusBarService, new StatusBarService());
|
||||
RegisterSingleton<HTMLService>(Services.HTMLService, new HTMLService());
|
||||
RegisterSingleton<SanitiserService>(Services.SanitiserService, new SanitiserService());
|
||||
RegisterSingleton<DiffService>(Services.DiffService, new DiffService());
|
||||
|
|
@ -79,17 +73,14 @@ export function RegisterAiProvider() {
|
|||
|
||||
if (provider == AIProvider.Claude) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new Claude());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new ClaudeTokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new ClaudeConversationNamingService());
|
||||
}
|
||||
else if (provider == AIProvider.Gemini) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new Gemini());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new GeminiTokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new GeminiConversationNamingService());
|
||||
}
|
||||
else if (provider == AIProvider.OpenAI) {
|
||||
RegisterSingleton<IAIClass>(Services.IAIClass, new OpenAI());
|
||||
RegisterSingleton<ITokenService>(Services.ITokenService, new OpenAITokenService());
|
||||
RegisterSingleton<IConversationNamingService>(Services.IConversationNamingService, new OpenAIConversationNamingService());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ export class Services {
|
|||
static SettingsService = Symbol("SettingsService");
|
||||
static EventService = Symbol("EventService");
|
||||
static AbortService = Symbol("AbortService");
|
||||
static StatusBarService = Symbol("StatusBarService");
|
||||
static HTMLService = Symbol("HTMLService");
|
||||
static VaultService = Symbol("VaultService");
|
||||
static VaultCacheService = Symbol("VaultCacheService");
|
||||
|
|
@ -28,7 +27,6 @@ export class Services {
|
|||
// interfaces
|
||||
static IAIClass = Symbol("IAIClass");
|
||||
static IPrompt = Symbol("IPrompt");
|
||||
static ITokenService = Symbol("ITokenService");
|
||||
static IConversationNamingService = Symbol("IConversationNamingService");
|
||||
|
||||
// modals
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
import type VaultkeeperAIPlugin from "main";
|
||||
import { Resolve } from "./DependencyService";
|
||||
import { Services } from "./Services";
|
||||
import { Component } from "obsidian";
|
||||
|
||||
export class StatusBarService extends Component {
|
||||
|
||||
private readonly plugin: VaultkeeperAIPlugin;
|
||||
private statusBarItem: HTMLElement | null;
|
||||
private currentInputTokens: number = 0;
|
||||
private currentOutputTokens: number = 0;
|
||||
private animationFrame: number | null = null;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
||||
}
|
||||
|
||||
override onunload(): void {
|
||||
this.removeStatusBarMessage();
|
||||
}
|
||||
|
||||
public setStatusBarMessage(message: string) {
|
||||
if (this.statusBarItem == null) {
|
||||
this.createStatusBarMessage();
|
||||
}
|
||||
|
||||
this.statusBarItem?.empty();
|
||||
this.statusBarItem?.createEl("span", { text: message });
|
||||
}
|
||||
|
||||
public animateTokens(targetInputTokens: number, targetOutputTokens: number) {
|
||||
// Cancel any ongoing animation
|
||||
if (this.animationFrame !== null) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
}
|
||||
|
||||
const startInputTokens = this.currentInputTokens;
|
||||
const startOutputTokens = this.currentOutputTokens;
|
||||
const startTime = performance.now();
|
||||
const duration = 1000;
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Ease-out cubic for smooth deceleration
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
|
||||
// Calculate current values
|
||||
this.currentInputTokens = Math.round(
|
||||
startInputTokens + (targetInputTokens - startInputTokens) * eased
|
||||
);
|
||||
this.currentOutputTokens = Math.round(
|
||||
startOutputTokens + (targetOutputTokens - startOutputTokens) * eased
|
||||
);
|
||||
|
||||
this.setStatusBarMessage(
|
||||
`Input Tokens: ${this.currentInputTokens} / Output Tokens: ${this.currentOutputTokens}`
|
||||
);
|
||||
|
||||
// Continue animation if not complete
|
||||
if (progress < 1) {
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
} else {
|
||||
this.animationFrame = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
public removeStatusBarMessage() {
|
||||
if (this.animationFrame !== null) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
this.statusBarItem?.remove();
|
||||
this.statusBarItem = null;
|
||||
}
|
||||
|
||||
private createStatusBarMessage() {
|
||||
this.statusBarItem?.remove();
|
||||
this.statusBarItem = this.plugin.addStatusBarItem();
|
||||
this.statusBarItem.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,6 @@ import { ItemView, WorkspaceLeaf } from 'obsidian';
|
|||
import { mount, unmount } from 'svelte';
|
||||
import ChatWindow from 'Components/ChatWindow.svelte';
|
||||
import TopBar from 'Components/TopBar.svelte';
|
||||
import type { StatusBarService } from 'Services/StatusBarService';
|
||||
import { Resolve } from 'Services/DependencyService';
|
||||
import { Services } from 'Services/Services';
|
||||
|
||||
export const VIEW_TYPE_MAIN = 'vaultkeeper-ai-main-view';
|
||||
|
||||
|
|
@ -15,8 +12,6 @@ interface ChatWindowComponent {
|
|||
|
||||
export class MainView extends ItemView {
|
||||
|
||||
private statusBarService: StatusBarService = Resolve<StatusBarService>(Services.StatusBarService);
|
||||
|
||||
constructor(leaf: WorkspaceLeaf) {
|
||||
super(leaf);
|
||||
}
|
||||
|
|
@ -40,7 +35,6 @@ export class MainView extends ItemView {
|
|||
const container = this.contentEl;
|
||||
container.empty();
|
||||
|
||||
// Mount TopBar with reference to ChatWindow's focus function
|
||||
this.topBar = mount(TopBar, {
|
||||
target: container,
|
||||
props: {
|
||||
|
|
@ -52,7 +46,6 @@ export class MainView extends ItemView {
|
|||
}
|
||||
});
|
||||
|
||||
// Mount ChatWindow first
|
||||
this.input = mount(ChatWindow, {
|
||||
target: container,
|
||||
props: {}
|
||||
|
|
@ -68,6 +61,5 @@ export class MainView extends ItemView {
|
|||
if (this.input) {
|
||||
await unmount(this.input);
|
||||
}
|
||||
this.statusBarService.removeStatusBarMessage();
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
let mockNamingService: any;
|
||||
let mockPrompt: any;
|
||||
let mockStatusBarService: any;
|
||||
let mockTokenService: any;
|
||||
let mockEventService: any;
|
||||
let abortService: AbortService;
|
||||
|
||||
|
|
@ -53,10 +52,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
animateTokens: vi.fn()
|
||||
};
|
||||
|
||||
mockTokenService = {
|
||||
countTokens: vi.fn().mockResolvedValue(100)
|
||||
};
|
||||
|
||||
// Mock EventService since it extends Obsidian's Events class
|
||||
mockEventService = {
|
||||
trigger: vi.fn(),
|
||||
|
|
@ -72,7 +67,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
RegisterSingleton(Services.AIFunctionService, mockAIFunctionService);
|
||||
RegisterSingleton(Services.ConversationNamingService, mockNamingService);
|
||||
RegisterSingleton(Services.IPrompt, mockPrompt);
|
||||
RegisterSingleton(Services.StatusBarService, mockStatusBarService);
|
||||
RegisterSingleton(Services.EventService, mockEventService);
|
||||
RegisterSingleton(Services.AbortService, abortService);
|
||||
|
||||
|
|
@ -102,7 +96,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
it('should resolve AI provider services', () => {
|
||||
const mockAI = { streamRequest: vi.fn() };
|
||||
RegisterSingleton(Services.IAIClass, mockAI as any);
|
||||
RegisterSingleton(Services.ITokenService, mockTokenService);
|
||||
|
||||
service.resolveAIProvider();
|
||||
|
||||
|
|
@ -125,90 +118,6 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setStatusBarTokens', () => {
|
||||
beforeEach(() => {
|
||||
// Need to resolve AI provider to initialize token service
|
||||
RegisterSingleton(Services.IAIClass, { streamRequest: vi.fn() } as any);
|
||||
RegisterSingleton(Services.ITokenService, mockTokenService);
|
||||
service.resolveAIProvider();
|
||||
});
|
||||
|
||||
it('should call status bar service with token counts', () => {
|
||||
service.setStatusBarTokens(100, 50);
|
||||
|
||||
expect(mockStatusBarService.animateTokens).toHaveBeenCalledWith(100, 50);
|
||||
});
|
||||
|
||||
it('should handle zero tokens', () => {
|
||||
service.setStatusBarTokens(0, 0);
|
||||
|
||||
expect(mockStatusBarService.animateTokens).toHaveBeenCalledWith(0, 0);
|
||||
});
|
||||
|
||||
it('should handle large token counts', () => {
|
||||
service.setStatusBarTokens(100000, 50000);
|
||||
|
||||
expect(mockStatusBarService.animateTokens).toHaveBeenCalledWith(100000, 50000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateTokenDisplay', () => {
|
||||
beforeEach(() => {
|
||||
// Need to resolve AI provider to initialize token service
|
||||
RegisterSingleton(Services.IAIClass, { streamRequest: vi.fn() } as any);
|
||||
RegisterSingleton(Services.ITokenService, mockTokenService);
|
||||
service.resolveAIProvider();
|
||||
});
|
||||
|
||||
it('should count tokens for conversation contents', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'User message'));
|
||||
conversation.contents.push(new ConversationContent(Role.Assistant, 'Assistant response'));
|
||||
|
||||
await service.updateTokenDisplay(conversation);
|
||||
|
||||
expect(mockTokenService.countTokens).toHaveBeenCalled();
|
||||
expect(mockStatusBarService.animateTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should include system and user instructions in token count', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Test'));
|
||||
|
||||
await service.updateTokenDisplay(conversation);
|
||||
|
||||
expect(mockPrompt.systemInstruction).toHaveBeenCalled();
|
||||
expect(mockPrompt.userInstruction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter out function call responses from user messages', async () => {
|
||||
const conversation = new Conversation();
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Regular message'));
|
||||
conversation.contents.push(
|
||||
new ConversationContent(Role.User, 'Function result', '', '', new Date(), false, true, 'tool-id')
|
||||
);
|
||||
|
||||
await service.updateTokenDisplay(conversation);
|
||||
|
||||
expect(mockTokenService.countTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty conversation', async () => {
|
||||
const conversation = new Conversation();
|
||||
|
||||
await service.updateTokenDisplay(conversation);
|
||||
|
||||
expect(mockStatusBarService.animateTokens).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw if token service not initialized', async () => {
|
||||
const newService = new ChatService();
|
||||
const conversation = new Conversation();
|
||||
|
||||
await expect(newService.updateTokenDisplay(conversation)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onNameChanged callback', () => {
|
||||
it('should allow setting callback function', () => {
|
||||
const callback = vi.fn();
|
||||
|
|
@ -327,7 +236,7 @@ describe('ChatService - Integration Tests (Sync Methods Only)', () => {
|
|||
conversation.contents.push(new ConversationContent(Role.User, 'Request'));
|
||||
conversation.contents.push(new ConversationContent(Role.Assistant, 'Making a function call', '', '{"name":"test"}', new Date(), true));
|
||||
// Function response (already User role with isFunctionCallResponse)
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Function result', 'Function result', '', new Date(), false, true, 'tool-1'));
|
||||
conversation.contents.push(new ConversationContent(Role.User, 'Function result', 'Function result', '', new Date(), false, true, false, 'tool-1'));
|
||||
// Assistant processes the function response
|
||||
conversation.contents.push(new ConversationContent(Role.Assistant, 'Final response'));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,356 +0,0 @@
|
|||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
||||
import { StatusBarService } from '../../Services/StatusBarService';
|
||||
import { RegisterSingleton, DeregisterAllServices } from '../../Services/DependencyService';
|
||||
import { Services } from '../../Services/Services';
|
||||
|
||||
describe('StatusBarService', () => {
|
||||
let service: StatusBarService;
|
||||
let mockPlugin: any;
|
||||
let mockStatusBarItem: any;
|
||||
let rafCallbacks: Array<(time: number) => void>;
|
||||
let currentTime: number;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
// Track RAF callbacks
|
||||
rafCallbacks = [];
|
||||
currentTime = 0;
|
||||
|
||||
// Mock requestAnimationFrame
|
||||
global.requestAnimationFrame = vi.fn((callback: (time: number) => void) => {
|
||||
rafCallbacks.push(callback);
|
||||
return rafCallbacks.length;
|
||||
});
|
||||
|
||||
// Mock cancelAnimationFrame
|
||||
global.cancelAnimationFrame = vi.fn((handle: number) => {
|
||||
if (handle > 0 && handle <= rafCallbacks.length) {
|
||||
rafCallbacks[handle - 1] = () => {}; // Clear callback
|
||||
}
|
||||
});
|
||||
|
||||
// Mock performance.now
|
||||
global.performance.now = vi.fn(() => currentTime);
|
||||
|
||||
// Mock status bar item
|
||||
mockStatusBarItem = {
|
||||
empty: vi.fn(),
|
||||
createEl: vi.fn(),
|
||||
remove: vi.fn()
|
||||
};
|
||||
|
||||
// Mock plugin
|
||||
mockPlugin = {
|
||||
addStatusBarItem: vi.fn().mockReturnValue(mockStatusBarItem)
|
||||
};
|
||||
RegisterSingleton(Services.VaultkeeperAIPlugin, mockPlugin);
|
||||
|
||||
service = new StatusBarService();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up service resources (cancel animations, remove DOM elements)
|
||||
service.removeStatusBarMessage();
|
||||
|
||||
// Clear singleton registry to prevent memory leaks
|
||||
DeregisterAllServices();
|
||||
|
||||
// Clear RAF callback references
|
||||
rafCallbacks = [];
|
||||
|
||||
// Restore all mocks
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Constructor and Initialization', () => {
|
||||
it('should initialize with dependencies', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not create status bar item until first use', () => {
|
||||
expect(mockPlugin.addStatusBarItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setStatusBarMessage', () => {
|
||||
it('should create status bar item on first call', () => {
|
||||
service.setStatusBarMessage('Test message');
|
||||
|
||||
expect(mockPlugin.addStatusBarItem).toHaveBeenCalled();
|
||||
expect(mockStatusBarItem.empty).toHaveBeenCalled();
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenCalledWith('span', { text: 'Test message' });
|
||||
});
|
||||
|
||||
it('should reuse existing status bar item on subsequent calls', () => {
|
||||
service.setStatusBarMessage('First message');
|
||||
service.setStatusBarMessage('Second message');
|
||||
|
||||
// Should only create once
|
||||
expect(mockPlugin.addStatusBarItem).toHaveBeenCalledTimes(1);
|
||||
// empty() is called: createStatusBarMessage(1) + setStatusBarMessage first call(2) + second call(3) = 3 times
|
||||
expect(mockStatusBarItem.empty).toHaveBeenCalledTimes(3);
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should update message content', () => {
|
||||
service.setStatusBarMessage('Message 1');
|
||||
service.setStatusBarMessage('Message 2');
|
||||
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenLastCalledWith('span', { text: 'Message 2' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeStatusBarMessage', () => {
|
||||
it('should remove status bar item', () => {
|
||||
service.setStatusBarMessage('Test');
|
||||
service.removeStatusBarMessage();
|
||||
|
||||
expect(mockStatusBarItem.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should cancel ongoing animation', () => {
|
||||
service.animateTokens(100, 200);
|
||||
expect(rafCallbacks.length).toBeGreaterThan(0);
|
||||
|
||||
service.removeStatusBarMessage();
|
||||
|
||||
expect(global.cancelAnimationFrame).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle being called when no status bar exists', () => {
|
||||
// Should not throw
|
||||
expect(() => service.removeStatusBarMessage()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should clear status bar reference', () => {
|
||||
service.setStatusBarMessage('Test');
|
||||
service.removeStatusBarMessage();
|
||||
|
||||
// Next call should recreate
|
||||
service.setStatusBarMessage('New message');
|
||||
expect(mockPlugin.addStatusBarItem).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('animateTokens', () => {
|
||||
it('should start animation with requestAnimationFrame', () => {
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
expect(global.requestAnimationFrame).toHaveBeenCalled();
|
||||
expect(rafCallbacks.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should animate from current values to target values', () => {
|
||||
// Start from 0, animate to 100 input and 200 output
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
// Run first frame at t=0
|
||||
currentTime = 0;
|
||||
rafCallbacks[0](currentTime);
|
||||
|
||||
// Should show start values
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenCalledWith('span', {
|
||||
text: 'Input Tokens: 0 / Output Tokens: 0'
|
||||
});
|
||||
|
||||
// Run frame at t=500 (halfway through 1000ms animation)
|
||||
currentTime = 500;
|
||||
rafCallbacks[1](currentTime);
|
||||
|
||||
// Should be roughly halfway (with easing)
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
const text = lastCall[1].text;
|
||||
expect(text).toContain('Input Tokens:');
|
||||
expect(text).toContain('Output Tokens:');
|
||||
});
|
||||
|
||||
it('should complete animation after 1000ms', () => {
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
// Simulate animation frames until completion
|
||||
currentTime = 0;
|
||||
let iterations = 0;
|
||||
const maxIterations = 100; // Safety limit
|
||||
|
||||
while (currentTime < 1000 && iterations < maxIterations) {
|
||||
const callbackIndex = rafCallbacks.length - 1; // Get most recent callback
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16; // ~60fps
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Run final frame at exactly 1000ms to ensure completion
|
||||
currentTime = 1000;
|
||||
const finalCallbackIndex = rafCallbacks.length - 1;
|
||||
if (finalCallbackIndex >= 0) {
|
||||
rafCallbacks[finalCallbackIndex](currentTime);
|
||||
}
|
||||
|
||||
// Should reach target values
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
expect(lastCall[1].text).toBe('Input Tokens: 100 / Output Tokens: 200');
|
||||
});
|
||||
|
||||
it('should cancel previous animation when starting new one', () => {
|
||||
service.animateTokens(50, 100);
|
||||
const firstAnimationFrame = rafCallbacks.length;
|
||||
|
||||
service.animateTokens(150, 250);
|
||||
|
||||
expect(global.cancelAnimationFrame).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle zero target values', () => {
|
||||
// Start with some values
|
||||
(service as any).currentInputTokens = 100;
|
||||
(service as any).currentOutputTokens = 200;
|
||||
|
||||
service.animateTokens(0, 0);
|
||||
|
||||
// Run to completion
|
||||
currentTime = 0;
|
||||
rafCallbacks[0](currentTime);
|
||||
currentTime = 1000;
|
||||
if (rafCallbacks.length > 1) {
|
||||
rafCallbacks[rafCallbacks.length - 1](currentTime);
|
||||
}
|
||||
|
||||
// Should animate down to zero
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
expect(lastCall[1].text).toContain('0');
|
||||
});
|
||||
|
||||
it('should handle large token jumps', () => {
|
||||
service.animateTokens(10000, 20000);
|
||||
|
||||
currentTime = 0;
|
||||
rafCallbacks[0](currentTime);
|
||||
|
||||
// Should not throw and should format correctly
|
||||
expect(mockStatusBarItem.createEl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should round token values during animation', () => {
|
||||
service.animateTokens(99, 199);
|
||||
|
||||
currentTime = 0;
|
||||
rafCallbacks[0](currentTime);
|
||||
|
||||
// All displayed values should be integers
|
||||
mockStatusBarItem.createEl.mock.calls.forEach((call: any) => {
|
||||
const text = call[1].text;
|
||||
const matches = text.match(/\d+/g);
|
||||
if (matches) {
|
||||
matches.forEach((num: string) => {
|
||||
expect(num).toBe(Math.round(parseInt(num)).toString());
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should use ease-out cubic easing', () => {
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
// Collect values at different times
|
||||
const values: number[] = [];
|
||||
|
||||
for (let t = 0; t <= 1000; t += 100) {
|
||||
currentTime = t;
|
||||
const currentIndex = rafCallbacks.length - 1;
|
||||
if (currentIndex >= 0) {
|
||||
rafCallbacks[currentIndex](currentTime);
|
||||
}
|
||||
|
||||
// Extract input token value
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
const match = lastCall[1].text.match(/Input Tokens: (\d+)/);
|
||||
if (match) {
|
||||
values.push(parseInt(match[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// With ease-out, changes should be larger at start than at end
|
||||
if (values.length >= 3) {
|
||||
const earlyChange = values[1] - values[0];
|
||||
const lateChange = values[values.length - 1] - values[values.length - 2];
|
||||
expect(earlyChange).toBeGreaterThan(lateChange);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Animation Cycles', () => {
|
||||
it('should handle rapid consecutive calls', () => {
|
||||
service.animateTokens(50, 100);
|
||||
service.animateTokens(100, 150);
|
||||
service.animateTokens(150, 200);
|
||||
|
||||
// Should cancel previous animations
|
||||
expect(global.cancelAnimationFrame).toHaveBeenCalled();
|
||||
|
||||
// Run the latest animation to completion
|
||||
currentTime = 1000;
|
||||
const latestCallback = rafCallbacks[rafCallbacks.length - 1];
|
||||
if (latestCallback) {
|
||||
latestCallback(currentTime);
|
||||
}
|
||||
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
expect(lastCall[1].text).toContain('150');
|
||||
expect(lastCall[1].text).toContain('200');
|
||||
});
|
||||
|
||||
it('should maintain state between animations', () => {
|
||||
// First animation - run to completion
|
||||
service.animateTokens(100, 200);
|
||||
|
||||
// Simulate animation to completion
|
||||
currentTime = 0;
|
||||
const startTime = currentTime;
|
||||
let iterations = 0;
|
||||
while (currentTime - startTime < 1000 && iterations < 100) {
|
||||
const callbackIndex = rafCallbacks.length - 1;
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Final frame at exactly 1000ms
|
||||
currentTime = startTime + 1000;
|
||||
const finalIndex = rafCallbacks.length - 1;
|
||||
if (finalIndex >= 0) {
|
||||
rafCallbacks[finalIndex](currentTime);
|
||||
}
|
||||
|
||||
// Second animation should start from previous end values
|
||||
rafCallbacks = []; // Reset RAF tracking
|
||||
service.animateTokens(200, 400);
|
||||
|
||||
// Simulate second animation to completion
|
||||
currentTime = performance.now();
|
||||
const secondStartTime = currentTime;
|
||||
iterations = 0;
|
||||
while (currentTime - secondStartTime < 1000 && iterations < 100) {
|
||||
const callbackIndex = rafCallbacks.length - 1;
|
||||
if (callbackIndex >= 0) {
|
||||
rafCallbacks[callbackIndex](currentTime);
|
||||
}
|
||||
currentTime += 16;
|
||||
iterations++;
|
||||
}
|
||||
|
||||
// Final frame at exactly 1000ms
|
||||
currentTime = secondStartTime + 1000;
|
||||
const secondFinalIndex = rafCallbacks.length - 1;
|
||||
if (secondFinalIndex >= 0) {
|
||||
rafCallbacks[secondFinalIndex](currentTime);
|
||||
}
|
||||
|
||||
const lastCall = mockStatusBarItem.createEl.mock.calls[mockStatusBarItem.createEl.mock.calls.length - 1];
|
||||
expect(lastCall[1].text).toBe('Input Tokens: 200 / Output Tokens: 400');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -52,7 +52,6 @@
|
|||
"diff2html": "^3.4.52",
|
||||
"express": "^5.2.1",
|
||||
"fuzzysort": "^3.1.0",
|
||||
"gpt-tokenizer": "^3.4.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"katex": "^0.16.27",
|
||||
"lowlight": "^3.3.0",
|
||||
|
|
|
|||
Loading…
Reference in a new issue