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 - will be overridden for specific AI endpoints timeout: 600000 // 10 minutes default timeout for AI operations }); // Request interceptor to add JWT token api.interceptors.request.use( (config) => { const token = localStorage.getItem('auth_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } // Debug logging for focus group updates if (config.method === 'put' && config.url?.includes('/focus-groups/')) { console.log('🌐 API Request:', { method: config.method, url: config.url, baseURL: config.baseURL, fullURL: `${config.baseURL}${config.url}`, data: config.data }); } // Debug logging for folder operations if (config.url?.includes('/folders/')) { console.log('🌐 API Folder Request:', { method: config.method, url: config.url, baseURL: config.baseURL, fullURL: `${config.baseURL}${config.url}`, data: config.data }); } 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 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'))); console.log('API Error:', { url: error.config?.url, method: error.config?.method, isPersonaRequest: isPersonaRequest }); // 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-')) { console.log('Cannot update with local ID, creating new instead:', id); 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; console.log(`Modifying persona with AI, ID: ${personaId}`); 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; console.log(`Deleting persona with ID: ${personaId}`); 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: 300000 // 5 minutes for profile 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: 600000 // 10 minutes for single persona generation }), // Generate and immediately save to database generateAndSave: (options?: any) => api.post('/ai-personas/generate-and-save', options || {}, { timeout: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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 ) => { try { // Log the API call with model information console.log(`📡 API call to generate-basic-profiles with model: ${llmModel || 'gemini-2.5-pro'}`); // 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: 600000 // 10 minutes for basic profile generation }); const basicProfiles = basicProfilesResponse.data.profiles; const personas = []; const personaIds = []; const errors = []; // Log the second stage API calls with model information console.log(`📡 API call to complete-and-save-persona with model: ${llmModel || 'gemini-2.5-pro'}`); // 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: 600000 // 10 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, 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: 600000 // 10 minutes timeout for AI enhancement }), // Batch generate summaries for download batchGenerateSummaries: (personaIds: string[], temperature: number = 0.7, llmModel?: string) => { // Log the API call with model information console.log(`📡 Frontend: API call to batch-generate-summaries with model: ${llmModel || 'gemini-2.5-pro'}`); return api.post('/ai-personas/batch-generate-summaries', { persona_ids: personaIds, temperature, llm_model: llmModel || 'gemini-2.5-pro' }, { timeout: 900000 // 15 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: 300000 // 5 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}`) }; // 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: 600000 // 10 minutes timeout for AI generation }), generateDiscussionGuideForGroup: (focusGroupId: string, data: any) => api.post(`/focus-groups/${focusGroupId}/generate-discussion-guide`, data, { timeout: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 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: 600000 // 10 minutes for LLM decision making }), getConversationInsights: (focusGroupId: string) => api.get(`/focus-group-ai/conversation/insights/${focusGroupId}`, { timeout: 600000 // 10 minutes for LLM insight generation }), 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[]) => { console.log(`🌐 API removePersonasBatch: Sending POST to /folders/${folderId}/personas/remove-batch with persona_ids:`, personaIds); return api.post(`/folders/${folderId}/personas/remove-batch`, { persona_ids: personaIds }); }, // 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"); } }; export default api;