// TypeScript API client for AI Voicebot server import { components } from "./api-types"; import { base } from "./Common"; // Re-export commonly used types from the generated schema export type LobbyModel = components["schemas"]["LobbyModel"]; export type LobbyListItem = components["schemas"]["LobbyListItem"]; export type LobbyCreateData = components["schemas"]["LobbyCreateData"]; export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"]; // Type aliases for API methods export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"]; export type AdminActionResponse = components["schemas"]["AdminActionResponse"]; export type AdminSetPassword = components["schemas"]["AdminSetPassword"]; export type AdminClearPassword = components["schemas"]["AdminClearPassword"]; export type HealthResponse = components["schemas"]["HealthResponse"]; export type LobbiesResponse = components["schemas"]["LobbiesResponse"]; export type SessionResponse = components["schemas"]["SessionResponse"]; export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"]; export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"]; // Bot Provider Types (manually defined until API types are regenerated) export interface BotInfoModel { name: string; description: string; } export interface BotProviderModel { provider_id: string; base_url: string; name: string; description: string; registered_at: number; last_seen: number; } export interface BotProviderListResponse { providers: BotProviderModel[]; } export interface BotListResponse { bots: BotInfoModel[]; providers: Record; } export interface BotJoinLobbyRequest { bot_name: string; lobby_id: string; nick?: string; provider_id?: string; } export interface BotJoinLobbyResponse { status: string; bot_name: string; run_id: string; provider_id: string; } export interface BotLeaveLobbyRequest { session_id: string; } export interface BotLeaveLobbyResponse { status: string; session_id: string; run_id?: string; } export class ApiError extends Error { constructor(public status: number, public statusText: string, public data?: any) { super(`HTTP ${status}: ${statusText}`); this.name = "ApiError"; } } export class ApiClient { private baseURL: string; private defaultHeaders: Record; constructor(baseURL?: string) { // Use the current window location instead of localhost, just like WebSocket connections const defaultBaseURL = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.host}` : "http://localhost:8001"; this.baseURL = baseURL || process.env.REACT_APP_API_URL || defaultBaseURL; this.defaultHeaders = {}; } /** * Construct API path using PUBLIC_URL environment variable * Replaces hardcoded /ai-voicebot prefix with dynamic base from environment */ private getApiPath(schemaPath: string): string { // Replace the hardcoded /ai-voicebot prefix with the dynamic base return schemaPath.replace("/ai-voicebot", base); } private async request( path: string, options: { method: string; body?: any; params?: Record; } ): Promise { const url = new URL(path, this.baseURL); if (options.params) { Object.entries(options.params).forEach(([key, value]) => { url.searchParams.append(key, value); }); } const requestInit: RequestInit = { method: options.method, headers: { "Content-Type": "application/json", ...this.defaultHeaders, }, }; if (options.body && options.method !== "GET") { requestInit.body = JSON.stringify(options.body); } const response = await fetch(url.toString(), requestInit); if (!response.ok) { let errorData; // Clone the response before trying to read it, in case JSON parsing fails const responseClone = response.clone(); try { errorData = await response.json(); } catch { try { errorData = await responseClone.text(); } catch { errorData = `HTTP ${response.status}: ${response.statusText}`; } } throw new ApiError(response.status, response.statusText, errorData); } const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { return response.json(); } return response.text() as unknown as T; } // Admin API methods async adminListNames(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/admin/names"), { method: "GET" }); } async adminSetPassword(data: AdminSetPassword): Promise { return this.request(this.getApiPath("/ai-voicebot/api/admin/set_password"), { method: "POST", body: data, }); } async adminClearPassword(data: AdminClearPassword): Promise { return this.request(this.getApiPath("/ai-voicebot/api/admin/clear_password"), { method: "POST", body: data, }); } // Health check async healthCheck(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" }); } // Session methods async getSession(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" }); } // Lobby methods async getLobbies(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/lobby"), { method: "GET" }); } async createLobby(sessionId: string, data: LobbyCreateRequest): Promise { return this.request(this.getApiPath(`/ai-voicebot/api/lobby/${sessionId}`), { method: "POST", body: data, }); } // Bot Provider methods async getBotProviders(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" }); } async getAvailableBots(): Promise { return this.request(this.getApiPath("/ai-voicebot/api/bots"), { method: "GET" }); } async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise { return this.request( this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`), { method: "POST", body: request, } ); } async requestBotLeaveLobby(request: BotLeaveLobbyRequest): Promise { return this.request(this.getApiPath("/ai-voicebot/api/bots/leave"), { method: "POST", body: request, }); } // Auto-generated endpoints will be added here by update-api-client.js // DO NOT MANUALLY EDIT BELOW THIS LINE // Auto-generated endpoints async lobbyCreate(session_id: string, data: any): Promise { return this.request(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data }); } } // API Evolution Detection System interface ApiEndpoint { path: string; method: string; implemented: boolean; } class ApiEvolutionChecker { private static instance: ApiEvolutionChecker; private checkedOnce = false; static getInstance(): ApiEvolutionChecker { if (!this.instance) { this.instance = new ApiEvolutionChecker(); } return this.instance; } private getImplementedEndpoints(): Set { // Define all endpoints that are currently implemented in ApiClient // This list is automatically updated by update-api-client.js return new Set([ 'GET:/ai-voicebot/api/admin/names', 'GET:/ai-voicebot/api/bots', 'GET:/ai-voicebot/api/bots/providers', 'GET:/ai-voicebot/api/health', 'GET:/ai-voicebot/api/lobby', 'GET:/ai-voicebot/api/session', 'POST:/ai-voicebot/api/admin/clear_password', 'POST:/ai-voicebot/api/admin/set_password', 'POST:/ai-voicebot/api/lobby/{sessionId}', 'POST:/ai-voicebot/api/lobby/{session_id}' ]); } private getAvailableEndpoints(): ApiEndpoint[] { // Extract all endpoints from the generated paths type const endpoints: ApiEndpoint[] = []; const implementedSet = this.getImplementedEndpoints(); // Type-safe extraction of paths from the generated schema const pathKeys = [ '/ai-voicebot/api/admin/names', '/ai-voicebot/api/admin/set_password', '/ai-voicebot/api/admin/clear_password', '/ai-voicebot/api/health', '/ai-voicebot/api/session', '/ai-voicebot/api/lobby', '/ai-voicebot/api/lobby/{session_id}', '/ai-voicebot/{path}' // Generic catch-all, we'll skip this one ] as const; pathKeys.forEach(path => { if (path === '/ai-voicebot/{path}') { // Skip the generic proxy endpoint return; } // Check each HTTP method that might be available for this path const possibleMethods = ['get', 'post', 'put', 'delete', 'patch'] as const; possibleMethods.forEach(method => { const endpointKey = `${method.toUpperCase()}:${path}`; const implemented = implementedSet.has(endpointKey); // We can't directly check if the method exists at runtime due to TypeScript compilation, // but we know from our grep search what exists. Let's be more explicit: const knownEndpoints: Record = { '/ai-voicebot/api/admin/names': ['GET'], '/ai-voicebot/api/admin/set_password': ['POST'], '/ai-voicebot/api/admin/clear_password': ['POST'], '/ai-voicebot/api/health': ['GET'], '/ai-voicebot/api/session': ['GET'], '/ai-voicebot/api/lobby': ['GET'], '/ai-voicebot/api/lobby/{session_id}': ['POST'] }; const pathMethods = knownEndpoints[path]; if (pathMethods && pathMethods.includes(method.toUpperCase())) { endpoints.push({ path, method: method.toUpperCase(), implemented }); } }); }); return endpoints; } checkForNewEndpoints(): void { if (this.checkedOnce) { return; // Only check once per session to avoid spam } this.checkedOnce = true; try { const endpoints = this.getAvailableEndpoints(); const unimplemented = endpoints.filter(ep => !ep.implemented); if (unimplemented.length > 0) { console.group('🚨 API Evolution Warning'); console.warn( `Found ${unimplemented.length} API endpoints that are not implemented in ApiClient:` ); unimplemented.forEach(endpoint => { console.warn(` • ${endpoint.method} ${endpoint.path}`); }); console.warn('Consider updating api-client.ts to implement these endpoints.'); console.warn('Available types can be found in api-types.ts'); console.groupEnd(); } else { console.info('✅ All available API endpoints are implemented in ApiClient'); } // Also check for potential parameter changes by looking at method signatures this.checkForParameterChanges(); } catch (error) { console.warn('Failed to check for API evolution:', error); } } private checkForParameterChanges(): void { // This is a simpler check - we could extend this to compare parameter schemas // For now, we'll just remind developers to check if their implementation matches the schema console.info('💡 Tip: Regularly compare your method implementations with the generated types in api-types.ts to ensure parameter compatibility'); } // Method to manually trigger a fresh check (useful for development) recheckEndpoints(): void { this.checkedOnce = false; this.checkForNewEndpoints(); } } // Export the checker for manual use if needed export const apiEvolutionChecker = ApiEvolutionChecker.getInstance(); // Utility functions for development export const devUtils = { /** * Manually check for API evolution and log results */ async checkApiEvolution() { if (process.env.NODE_ENV !== 'development') { console.warn('API evolution checking is only available in development mode'); return; } try { const { advancedApiChecker } = await import('./api-evolution-checker'); const evolution = await advancedApiChecker.checkSchemaEvolution(); console.group('🔍 Manual API Evolution Check'); console.log('Unimplemented endpoints:', evolution.unimplementedEndpoints.length); console.log('New endpoints:', evolution.newEndpoints.length); console.log('Schema changed:', evolution.hasChangedEndpoints); if (evolution.unimplementedEndpoints.length > 0) { console.log('\nImplementation stubs:'); console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints)); } console.groupEnd(); return evolution; } catch (error) { console.error('Failed to check API evolution:', error); return null; } }, /** * Force a recheck of endpoints (bypasses the "once per session" limitation) */ recheckEndpoints() { apiEvolutionChecker.recheckEndpoints(); } }; // Default client instance export const apiClient = new ApiClient(); // Convenience API namespaces export const adminApi = { listNames: () => apiClient.adminListNames(), setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data), clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data), }; export const healthApi = { check: () => apiClient.healthCheck() }; export const lobbiesApi = { getAll: () => apiClient.getLobbies() }; export const sessionsApi = { getCurrent: () => apiClient.getSession(), createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data), }; export const botsApi = { getProviders: () => apiClient.getBotProviders(), getAvailable: () => apiClient.getAvailableBots(), requestJoinLobby: (botName: string, request: BotJoinLobbyRequest) => apiClient.requestBotJoinLobby(botName, request), }; // Automatically check for API evolution when this module is loaded // This will warn developers if new endpoints are available but not implemented if (process.env.NODE_ENV === 'development') { // Import the advanced checker dynamically to avoid circular dependencies import('./api-evolution-checker').then(({ advancedApiChecker }) => { // Run the check after a short delay to ensure all modules are loaded setTimeout(async () => { try { const evolution = await advancedApiChecker.checkSchemaEvolution(); if (evolution.unimplementedEndpoints.length > 0 || evolution.hasNewEndpoints) { console.group('🚨 API Evolution Detection'); if (evolution.hasNewEndpoints && evolution.newEndpoints.length > 0) { console.warn('🆕 New API endpoints detected:'); evolution.newEndpoints.forEach(ep => { console.warn(` • ${ep.method} ${ep.path} (${ep.operationId})`); }); } if (evolution.unimplementedEndpoints.length > 0) { console.warn('⚠️ Unimplemented API endpoints:'); evolution.unimplementedEndpoints.forEach(ep => { console.warn(` • ${ep.method} ${ep.path}`); }); } if (evolution.hasChangedEndpoints) { console.warn('🔄 API schema has changed - check for parameter updates'); } console.groupCollapsed('💡 Implementation suggestions:'); console.log('Add these methods to ApiClient:'); console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints)); console.groupEnd(); console.groupEnd(); } else { console.info('✅ All API endpoints are implemented'); } } catch (error) { console.warn('API evolution check failed:', error); // Fallback to simple checker apiEvolutionChecker.checkForNewEndpoints(); } }, 1000); }); }