semblance-dev/src/lib/api.ts

667 lines
No EOL
23 KiB
TypeScript

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;