import axios from 'axios'; // Base URL for API requests const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/semblance_back/api'; // Create axios instance with baseURL const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json' }, // Default timeout for regular endpoints; AI endpoints override below timeout: 120000 // 2 minutes }); // Helper function to check if JWT token is expired const isTokenExpired = (token: string): boolean => { // Offline mode token is never expired if (localStorage.getItem('offline_mode') === 'true') return false; try { const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Date.now() / 1000; return payload.exp < currentTime; } catch (error) { console.error('Error parsing JWT token:', error); return true; // Treat malformed tokens as expired } }; // Request interceptor to add JWT token api.interceptors.request.use( (config) => { const token = localStorage.getItem('auth_token'); if (token) { // Check if token is expired before making request if (isTokenExpired(token)) { localStorage.removeItem('auth_token'); localStorage.removeItem('user'); localStorage.removeItem('auth_type'); // Dispatch auth error to trigger redirect dispatchAuthError({ source: config.url }); // Reject the request return Promise.reject(new Error('Token expired')); } config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Create a custom event for auth errors export const AUTH_ERROR_EVENT = 'auth_error'; // Custom event with details export interface AuthErrorDetail { source?: string; isPersonaCreation?: boolean; } // Function to dispatch auth error event export const dispatchAuthError = (details?: AuthErrorDetail) => { // Only remove auth data if not a persona creation request if (!details?.isPersonaCreation) { // Clean up local storage auth data localStorage.removeItem('auth_token'); localStorage.removeItem('user'); } // Dispatch event for handling in auth context const event = new CustomEvent(AUTH_ERROR_EVENT, { detail: details || {} }); window.dispatchEvent(event); }; // Response interceptor to handle common errors api.interceptors.response.use( (response) => response, (error) => { // Handle 402 (Quota Exceeded) if (error.response && error.response.status === 402) { const data = error.response.data; window.dispatchEvent(new CustomEvent('quota_exceeded', { detail: { scope: data.scope, limit_usd: data.limit_usd, used_usd: data.used_usd } })); } // Handle 401 (Unauthorized) - dispatch event instead of redirect if (error.response && error.response.status === 401) { // Check if this is a persona-related request - these are handled separately const isPersonaRequest = error.config && (error.config.url?.includes('/personas') || error.config.url?.includes('/personas/batch') || (error.config.method && error.config.url?.startsWith('/personas'))); // For persona-related requests, don't automatically log out // Let the component handle the auth error if (!isPersonaRequest) { dispatchAuthError({ source: error.config?.url, isPersonaCreation: false }); } else { console.warn('Authentication error in persona request, letting component handle it'); } } return Promise.reject(error); } ); // Auth endpoints export const authApi = { login: (username: string, password: string) => api.post('/auth/login', { username, password }), loginWithMicrosoft: (idToken: string) => api.post('/auth/microsoft', { id_token: idToken }), register: (username: string, email: string, password: string) => api.post('/auth/register', { username, email, password }), getProfile: () => api.get('/auth/me') }; // Personas endpoints export const personasApi = { getAll: () => api.get('/personas/all'), getById: (id: string) => api.get(`/personas/${id}`), create: (personaData: any) => api.post('/personas', personaData), update: (id: string, personaData: any) => { // Don't try to update with local-prefixed IDs - they won't work if (id && id.startsWith('local-')) { return api.post('/personas', personaData); } return api.put(`/personas/${id}`, personaData); }, modifyWithAI: (id: string, modificationData: any) => { const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id; return api.post(`/personas/${personaId}/modify-with-ai`, modificationData); }, delete: (id: string) => { // Handle different ID formats // If the ID is an object with an _id property, use that // Otherwise, use the provided ID string const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id; return api.delete(`/personas/${personaId}`); }, createBatch: (personasData: any[]) => api.post('/personas/batch', personasData), // Export individual persona profile as markdown exportProfile: (id: string, options?: { llm_model?: string; temperature?: number }) => api.post(`/personas/${id}/export-profile`, options || {}, { timeout: 180000 // 3 minutes for profile export }), // Bulk export personas to specified format bulkExportPersonas: (data: { persona_ids: string[]; export_format: 'markdown' | 'json' | 'csv'; }) => api.post('/personas/bulk-export', data, { timeout: 180000 // 3 minutes for bulk export }) }; // AI Persona Generation endpoints export const aiPersonasApi = { // Legacy endpoints // Generate a persona without saving generate: (options?: any) => api.post('/ai-personas/generate', options || {}, { timeout: 180000 // 3 minutes for single persona generation }), // Generate and immediately save to database generateAndSave: (options?: any) => api.post('/ai-personas/generate-and-save', options || {}, { timeout: 180000 // 3 minutes for single persona generation }), // Generate multiple personas without saving batchGenerate: (options: { count: number, customizations?: any[], temperature?: number }) => api.post('/ai-personas/batch-generate', options, { timeout: 180000 // 3 minutes for batch generation }), // Generate multiple personas and save to database batchGenerateAndSave: (options: { count: number, customizations?: any[], temperature?: number }) => api.post('/ai-personas/batch-generate-and-save', options, { timeout: 180000 // 3 minutes for batch generation and save }), // Two-stage generation endpoints // First stage: Generate basic profiles generateBasicProfiles: ( audienceBrief: string, count: number = 5, temperature: number = 0.8 ) => api.post('/ai-personas/generate-basic-profiles', { audience_brief: audienceBrief, count, temperature }, { timeout: 180000 // 3 minutes for basic profile generation }), // Second stage: Complete a single persona without saving completePersona: (basicProfile: any, temperature: number = 0.7) => api.post('/ai-personas/complete-persona', { basic_profile: basicProfile, temperature }, { timeout: 180000 // 3 minutes for detailed persona generation }), // Second stage: Complete a single persona and save to database completeAndSavePersona: ( basicProfile: any, temperature: number = 0.7, audienceBrief?: string, researchObjective?: string, customerDataSessionId?: string, llmModel?: string ) => api.post('/ai-personas/complete-and-save-persona', { basic_profile: basicProfile, temperature, audience_brief: audienceBrief, research_objective: researchObjective, customer_data_session_id: customerDataSessionId, llm_model: llmModel }, { timeout: 180000 // 3 minutes for detailed persona generation and saving }), // Generate summary for an existing persona generatePersonaSummary: (personaData: any, temperature: number = 0.7) => api.post('/ai-personas/generate-persona-summary', { persona_data: personaData, temperature }, { timeout: 180000 // 3 minutes for summary generation }), // Helper method for two-stage batch generation and save batchGenerateWithStages: async ( audienceBrief: string, researchObjective?: string, count: number = 5, temperature: number = 0.7, customerDataSessionId?: string, llmModel?: string, onTaskIdReceived?: (taskId: string) => void ) => { try { // First stage: Generate basic profiles const basicProfilesResponse = await api.post('/ai-personas/generate-basic-profiles', { audience_brief: audienceBrief, research_objective: researchObjective, count, temperature: 0.7, // Use 0.7 temperature for basic profiles customer_data_session_id: customerDataSessionId, llm_model: llmModel || 'gemini-2.5-pro' }, { timeout: 180000 // 3 minutes for basic profile generation }); const basicProfiles = basicProfilesResponse.data.profiles; const taskId = basicProfilesResponse.data.task_id; // Extract task_id from first call // Call the callback immediately with the task ID if (taskId && onTaskIdReceived) { onTaskIdReceived(taskId); } const personas = []; const personaIds = []; const errors = []; // Second stage: Complete each profile in parallel const completeRequests = basicProfiles.map(profile => api.post('/ai-personas/complete-and-save-persona', { basic_profile: profile, temperature, audience_brief: audienceBrief, research_objective: researchObjective, customer_data_session_id: customerDataSessionId, llm_model: llmModel || 'gemini-2.5-pro' }, { timeout: 180000 // 3 minutes for each persona completion }) ); // Wait for all requests to complete const results = await Promise.allSettled(completeRequests); // Process results results.forEach((result, index) => { if (result.status === 'fulfilled') { personas.push(result.value.data.persona); personaIds.push(result.value.data.persona_id); } else { const basicProfile = basicProfiles[index]; const errorInfo = { index, name: basicProfile.name || `Persona ${index + 1}`, error: result.reason }; errors.push(errorInfo); console.error(`Failed to complete persona ${index + 1} (${basicProfile.name || 'unnamed'}):`, result.reason); } }); // Only fail completely if ALL personas failed if (personas.length === 0 && errors.length > 0) { throw new Error(`Failed to generate any personas. ${errors.length} profile(s) failed.`); } return { data: { message: `Generated and saved ${personas.length} personas${errors.length > 0 ? ` (${errors.length} failed)` : ''}`, personas, persona_ids: personaIds, task_id: taskId, errors: errors.length > 0 ? errors : undefined, partial_success: errors.length > 0 && personas.length > 0 } }; } catch (error) { // For errors in the first stage if (error.response?.status === 504 || error.code === "ECONNABORTED") { throw new Error(`Timeout error: The server took too long to generate personas. Please try with fewer personas or try again later.`); } throw error; } }, // Enhance audience brief endpoint enhanceAudienceBrief: (audienceBrief: string, researchObjective: string, temperature: number = 0.7) => api.post('/ai-personas/enhance-audience-brief', { audience_brief: audienceBrief, research_objective: researchObjective, temperature }, { timeout: 180000 // 3 minutes timeout for AI enhancement }), // Batch generate summaries for download batchGenerateSummaries: (personaIds: string[], temperature: number = 0.7, llmModel?: string) => { return api.post('/ai-personas/batch-generate-summaries', { persona_ids: personaIds, temperature, llm_model: llmModel || 'gemini-2.5-pro' }, { timeout: 180000 // 3 minutes timeout for batch processing }); }, // Upload customer data files for parsing uploadCustomerData: (files: FileList) => { const formData = new FormData(); for (let i = 0; i < files.length; i++) { formData.append('files', files[i]); } return api.post('/ai-personas/upload-customer-data', formData, { headers: { 'Content-Type': 'multipart/form-data', }, timeout: 180000 // 3 minutes for file upload and parsing }); }, // Clean up customer data files for a session cleanupCustomerData: (sessionId: string) => api.delete(`/ai-personas/cleanup-customer-data/${sessionId}`), // Unified persona generation endpoint with cancellation support generatePersonasFull: async ( audienceBrief: string, researchObjective?: string, count: number = 5, temperature: number = 0.8, customerDataSessionId?: string, llmModel?: string, targetFolderId?: string ) => { return api.post('/ai-personas/generate-personas-full', { audience_brief: audienceBrief, research_objective: researchObjective, count, temperature, customer_data_session_id: customerDataSessionId, llm_model: llmModel || 'gemini-2.5-pro', target_folder_id: targetFolderId }, { timeout: 10000 // 10 seconds — endpoint returns immediately with task_id }); } }; // Focus Groups endpoints export const focusGroupsApi = { getAll: () => api.get('/focus-groups'), getById: (id: string) => api.get(`/focus-groups/${id}`), create: (focusGroupData: any) => api.post('/focus-groups', focusGroupData), update: (id: string, focusGroupData: any) => api.put(`/focus-groups/${id}`, focusGroupData), delete: (id: string) => api.delete(`/focus-groups/${id}`), addParticipant: (focusGroupId: string, personaId: string) => api.post(`/focus-groups/${focusGroupId}/participants`, { persona_id: personaId }), removeParticipant: (focusGroupId: string, personaId: string) => api.delete(`/focus-groups/${focusGroupId}/participants/${personaId}`), sendMessage: (focusGroupId: string, messageData: any) => api.post(`/focus-groups/${focusGroupId}/messages`, messageData), getMessages: (focusGroupId: string) => api.get(`/focus-groups/${focusGroupId}/messages`), updateMessageHighlight: (focusGroupId: string, messageId: string, highlighted: boolean) => api.patch(`/focus-groups/${focusGroupId}/messages/${messageId}`, { highlighted }), describeAsset: (focusGroupId: string, assetFilename: string) => api.post(`/focus-groups/${focusGroupId}/describe-asset`, { asset_filename: assetFilename }, { timeout: 120000 // 2 minutes timeout for AI description generation }), generateDiscussionGuide: (data: any) => api.post('/focus-groups/generate-discussion-guide', data, { timeout: 180000 // 3 minutes timeout for AI generation }), generateDiscussionGuideForGroup: (focusGroupId: string, data: any) => api.post(`/focus-groups/${focusGroupId}/generate-discussion-guide`, data, { timeout: 180000 // 3 minutes timeout for AI generation }), downloadDiscussionGuide: async (focusGroupId: string) => { try { const response = await api.get(`/focus-groups/${focusGroupId}/discussion-guide/download`, { responseType: 'blob', // Important for file download timeout: 30000 // 30 seconds timeout for download }); // Extract filename from Content-Disposition header const contentDisposition = response.headers['content-disposition']; let filename = 'discussion-guide.md'; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename="([^"]+)"/); if (filenameMatch) { filename = filenameMatch[1]; } } // Create blob URL and trigger download const blob = new Blob([response.data], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); // Create temporary anchor element for download const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.style.display = 'none'; // Trigger download document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); // Clean up URL.revokeObjectURL(url); return { success: true, filename }; } catch (error) { console.error('Error downloading discussion guide:', error); throw new Error('Failed to download discussion guide'); } }, // Notes endpoints createNote: (focusGroupId: string, noteData: any) => api.post(`/focus-groups/${focusGroupId}/notes`, noteData), getNotes: (focusGroupId: string) => api.get(`/focus-groups/${focusGroupId}/notes`), deleteNote: (focusGroupId: string, noteId: string) => api.delete(`/focus-groups/${focusGroupId}/notes/${noteId}`), // Asset management endpoints uploadAssets: (focusGroupId: string, formData: FormData, replace?: boolean) => { // Add replace flag to form data if specified if (replace === true) { formData.append('replace', 'true'); } return api.post(`/focus-groups/${focusGroupId}/assets`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 120000 // 2 minutes for file upload }); }, getAssets: (focusGroupId: string) => api.get(`/focus-groups/${focusGroupId}/assets`), getAssetUrl: (focusGroupId: string, filename: string) => `${API_BASE_URL}/focus-groups/${focusGroupId}/assets/${filename}`, updateAssetName: (focusGroupId: string, filename: string, userAssignedName: string) => api.patch(`/focus-groups/${focusGroupId}/assets/${filename}`, { user_assigned_name: userAssignedName }), deleteAsset: (focusGroupId: string, filename: string) => api.delete(`/focus-groups/${focusGroupId}/assets/${filename}`) }; // Focus Group AI endpoints export const focusGroupAiApi = { generateResponse: ( focusGroupId: string, personaId: string, currentTopic: string, temperature: number = 0.7 ) => api.post('/focus-group-ai/generate-response', { focus_group_id: focusGroupId, persona_id: personaId, current_topic: currentTopic, temperature: temperature }, { timeout: 180000 // 3 minutes for AI response generation }), generateKeyThemes: ( focusGroupId: string, temperature: number = 0.7 ) => api.post('/focus-group-ai/generate-key-themes', { focus_group_id: focusGroupId, temperature: temperature }, { timeout: 180000 // 3 minutes for key theme generation }), getKeyThemes: (focusGroupId: string) => api.get(`/focus-group-ai/key-themes/${focusGroupId}`), deleteKeyTheme: (focusGroupId: string, themeId: string) => api.delete(`/focus-group-ai/key-themes/${focusGroupId}/${themeId}`), // AI Moderator endpoints getModeratorStatus: (focusGroupId: string) => api.get(`/focus-group-ai/moderator/status/${focusGroupId}`), advanceModeratorDiscussion: (focusGroupId: string) => api.post(`/focus-group-ai/moderator/advance/${focusGroupId}`, {}, { timeout: 180000 // 3 minutes for AI moderator response }), setModeratorPosition: (focusGroupId: string, sectionId: string, itemId?: string) => api.put(`/focus-group-ai/moderator/position/${focusGroupId}`, { section_id: sectionId, item_id: itemId }), // Autonomous Conversation endpoints startAutonomousConversation: (focusGroupId: string, initialPrompt?: string) => api.post(`/focus-group-ai/autonomous/start/${focusGroupId}`, { initial_prompt: initialPrompt }, { timeout: 180000 // 3 minutes for autonomous conversation startup }), stopAutonomousConversation: (focusGroupId: string, reason?: string) => api.post(`/focus-group-ai/autonomous/stop/${focusGroupId}`, { reason: reason }), getAutonomousConversationStatus: (focusGroupId: string) => api.get(`/focus-group-ai/autonomous/status/${focusGroupId}`), // Conversation State and Analytics endpoints getConversationState: (focusGroupId: string) => api.get(`/focus-group-ai/conversation/state/${focusGroupId}`), getConversationAnalytics: (focusGroupId: string) => api.get(`/focus-group-ai/conversation/analytics/${focusGroupId}`), makeConversationDecision: (focusGroupId: string, temperature: number = 0.7, mode: string = 'ai') => api.post(`/focus-group-ai/conversation/decision/${focusGroupId}`, { temperature: temperature, mode: mode }, { timeout: 10000 // Returns 202 immediately }), getConversationInsights: (focusGroupId: string) => api.get(`/focus-group-ai/conversation/insights/${focusGroupId}`, { timeout: 180000 // LLM call, keep long timeout }), manualIntervention: (focusGroupId: string, action: string, message?: string, participantId?: string) => api.post(`/focus-group-ai/conversation/intervene/${focusGroupId}`, { action: action, message: message, participant_id: participantId }), getReasoningHistory: (focusGroupId: string) => api.get(`/focus-group-ai/conversation/reasoning-history/${focusGroupId}`), // Session ending endpoint endSession: (focusGroupId: string, reason?: string) => api.post(`/focus-group-ai/moderator/end-session/${focusGroupId}`, { reason: reason || 'session_ended' }) }; // Folders endpoints export const foldersApi = { getAll: () => api.get('/folders'), getById: (id: string) => api.get(`/folders/${id}`), create: (folderData: any) => api.post('/folders', folderData), update: (id: string, folderData: any) => api.put(`/folders/${id}`, folderData), delete: (id: string) => api.delete(`/folders/${id}`), addPersona: (folderId: string, personaId: string) => api.post(`/folders/${folderId}/personas`, { persona_id: personaId }), removePersona: (folderId: string, personaId: string) => api.delete(`/folders/${folderId}/personas/${personaId}`), addPersonasBatch: (folderId: string, personaIds: string[]) => api.post(`/folders/${folderId}/personas/batch`, { persona_ids: personaIds }), removePersonasBatch: (folderId: string, personaIds: string[]) => { return api.post(`/folders/${folderId}/personas/remove-batch`, { persona_ids: personaIds }); }, // Hierarchical operations moveFolder: (folderId: string, parentFolderId: string | null) => api.put(`/folders/${folderId}/move`, { parent_folder_id: parentFolderId }), getDescendants: (folderId: string) => api.get(`/folders/${folderId}/descendants`), // New endpoints for multiple folder management addPersonaToMultipleFolders: (personaId: string, folderIds: string[]) => { // Add persona to multiple folders const promises = folderIds.map(folderId => api.post(`/folders/${folderId}/personas`, { persona_id: personaId }) ); return Promise.all(promises); }, removePersonaFromAllFolders: (personaId: string) => { // This would require getting all folders first, then removing from each // For now, we'll handle this on a per-folder basis in the UI throw new Error("Use removePersona for specific folders"); } }; // ─── Admin API ──────────────────────────────────────────────────────────────── export const adminApi = { // Users listUsers: (params?: { q?: string; role?: string; skip?: number; limit?: number }) => api.get('/admin/users', { params }), getUser: (id: string) => api.get(`/admin/users/${id}`), updateUser: (id: string, data: { role?: string; is_active?: boolean; quota?: { monthly_usd?: number }; override_quota?: boolean }) => api.put(`/admin/users/${id}`, data), disableUser: (id: string) => api.post(`/admin/users/${id}/disable`), enableUser: (id: string) => api.post(`/admin/users/${id}/enable`), // Usage usageSummary: (params?: { from?: string; to?: string; group_by?: string; user_id?: string; focus_group_id?: string }) => api.get('/admin/usage/summary', { params }), usageEvents: (params?: { user_id?: string; focus_group_id?: string; feature?: string; skip?: number; limit?: number }) => api.get('/admin/usage/events', { params }), // Pricing listPricing: () => api.get('/admin/pricing'), createPricing: (data: any) => api.post('/admin/pricing', data), // Users — create + reset password createUser: (data: { username: string; email: string; password: string; role?: string }) => api.post('/admin/users', data), resetPassword: (id: string, password: string) => api.post(`/admin/users/${id}/reset-password`, { password }), // Focus Groups (admin view) listFocusGroups: (params?: { skip?: number; limit?: number }) => api.get('/admin/focus-groups', { params }), }; export const usageApi = { me: () => api.get('/auth/me'), }; export default api;