import { Persona } from "@/types/persona"; import { aiPersonasApi, personasApi } from "@/lib/api"; /** * Generate synthetic personas using the AI endpoint * Using a two-stage approach: * 1. Generate basic profiles in one call * 2. Expand each profile into a full persona in parallel * * @param brief Audience brief to guide persona generation * @param researchObjective Optional research objective to focus persona goals and scenarios * @param count Number of personas to generate * @param file Optional data file to assist in generation (not currently used) * @param targetFolderId Optional folder ID to assign to generated personas * @returns Array of generated personas */ export async function generateSyntheticPersonas( brief: string, researchObjective?: string, count: number, file?: FileList, targetFolderId?: string | null ): Promise { // Debug logging for folder console.log(`generateSyntheticPersonas called with targetFolderId: ${targetFolderId || 'none'}`); try { // We'll use the two-stage approach which leverages parallel processing console.log(`Generating ${count} synthetic personas using two-stage approach...`); // First, check if the brief is too short to be useful if (brief.trim().length < 10) { throw new Error("Audience brief is too short. Please provide more context for better persona generation."); } // Upload customer data if files provided let customerDataSessionId: string | undefined; if (file && file.length > 0) { console.log(`Uploading ${file.length} customer data files...`); try { const uploadResponse = await aiPersonasApi.uploadCustomerData(file); customerDataSessionId = uploadResponse.data.session_id; console.log(`Customer data uploaded with session ID: ${customerDataSessionId}`); } catch (error) { console.error('Failed to upload customer data:', error); throw new Error("Failed to upload customer data files. Please try again."); } } // Use the batchGenerateWithStages helper that implements the two-stage process const response = await aiPersonasApi.batchGenerateWithStages( brief, // Pass the full audience brief for context researchObjective, // Pass the research objective for focused goals and scenarios count, // Number of personas to generate 0.8, // Temperature - slightly higher for more creativity customerDataSessionId // Pass customer data session ID if available ); if (response.data) { // Handle partial success (some personas succeeded, some failed) const hasPartialSuccess = response.data.partial_success === true; const hasPersonas = response.data.personas && response.data.personas.length > 0; const hasErrors = response.data.errors && response.data.errors.length > 0; if (hasPersonas) { console.log(`Generated ${response.data.personas.length} personas with two-stage process${hasErrors ? ` (${response.data.errors.length} failed)` : ''}`); // If a target folder ID is provided, add it to each persona if (targetFolderId) { const personas = response.data.personas as Persona[]; // Update each persona with the target folder ID const personasWithFolder = personas.map(persona => ({ ...persona, folderId: targetFolderId })); // Update personas in the database with the folder ID try { const updatePromises = personasWithFolder.map(persona => { if (persona.id || persona._id) { // Find the right ID to use const idToUse = persona._id || persona.id; console.log(`Updating persona ${persona.name || idToUse} with folder ID: ${targetFolderId}`); // Use personasApi.update instead of aiPersonasApi.completePersona return personasApi.update(idToUse, { ...persona, folderId: targetFolderId }).catch(error => { console.error(`Error updating folder ID for persona ${persona.name || idToUse}:`, error); return null; // Continue despite errors }); } return Promise.resolve(null); }); // Wait for all updates to complete, but don't fail if some fail await Promise.allSettled(updatePromises); console.log(`Added ${personasWithFolder.length} personas to folder ID: ${targetFolderId}`); } catch (error) { console.error("Error updating personas with folder ID:", error); // Continue anyway since we have the personas with folder ID in memory } // Clean up customer data files if session was created if (customerDataSessionId) { try { await aiPersonasApi.cleanupCustomerData(customerDataSessionId); console.log(`Cleaned up customer data for session: ${customerDataSessionId}`); } catch (cleanupError) { console.warn('Failed to cleanup customer data:', cleanupError); // Don't throw here, just log the warning } } // Return the personas with folder IDs and include any error information if (hasPartialSuccess || hasErrors) { // Need to ensure our response structure matches what the component expects return { ...response.data, // Keep the original structure personas: personasWithFolder, // Replace with our folder-updated personas length: personasWithFolder.length }; } // Return in the expected format return { ...response.data, personas: personasWithFolder }; } // Clean up customer data files if session was created if (customerDataSessionId) { try { await aiPersonasApi.cleanupCustomerData(customerDataSessionId); console.log(`Cleaned up customer data for session: ${customerDataSessionId}`); } catch (cleanupError) { console.warn('Failed to cleanup customer data:', cleanupError); // Don't throw here, just log the warning } } // Return the personas including any error information if (hasPartialSuccess || hasErrors) { return { ...response.data.personas, length: response.data.personas.length, partial_success: hasPartialSuccess, errors: response.data.errors }; } // Clean up customer data files if session was created if (customerDataSessionId) { try { await aiPersonasApi.cleanupCustomerData(customerDataSessionId); console.log(`Cleaned up customer data for session: ${customerDataSessionId}`); } catch (cleanupError) { console.warn('Failed to cleanup customer data:', cleanupError); // Don't throw here, just log the warning } } return response.data.personas as Persona[]; } else if (hasErrors) { // Clean up customer data files if session was created if (customerDataSessionId) { try { await aiPersonasApi.cleanupCustomerData(customerDataSessionId); console.log(`Cleaned up customer data for session: ${customerDataSessionId}`); } catch (cleanupError) { console.warn('Failed to cleanup customer data:', cleanupError); } } // If we have errors but no personas, throw an error throw new Error(`Failed to generate personas: ${response.data.errors.length} generation attempts failed.`); } else { throw new Error("No personas returned from API"); } } else { throw new Error("Invalid response format from API"); } } catch (error) { // Clean up customer data files if session was created if (customerDataSessionId) { try { await aiPersonasApi.cleanupCustomerData(customerDataSessionId); console.log(`Cleaned up customer data for session: ${customerDataSessionId}`); } catch (cleanupError) { console.warn('Failed to cleanup customer data:', cleanupError); } } console.error("Error generating AI personas:", error); throw error; } } /** * Extract possible personality traits from the research brief * @param brief The research brief text * @returns A string of personality traits */ function extractPersonalityFromBrief(brief: string): string | undefined { const traitKeywords = [ { keyword: 'creativ', trait: 'creative' }, { keyword: 'innovat', trait: 'innovative' }, { keyword: 'careful', trait: 'careful' }, { keyword: 'cautious', trait: 'cautious' }, { keyword: 'risk', trait: 'risk-taking' }, { keyword: 'adventur', trait: 'adventurous' }, { keyword: 'conserv', trait: 'conservative' }, { keyword: 'tradition', trait: 'traditional' }, { keyword: 'modern', trait: 'modern' }, { keyword: 'tech', trait: 'tech-savvy' }, { keyword: 'social', trait: 'social' }, { keyword: 'outgoing', trait: 'outgoing' }, { keyword: 'shy', trait: 'shy' }, { keyword: 'intro', trait: 'introverted' }, { keyword: 'extro', trait: 'extroverted' } ]; const briefLower = brief.toLowerCase(); const matchedTraits = traitKeywords .filter(item => briefLower.includes(item.keyword)) .map(item => item.trait); return matchedTraits.length > 0 ? matchedTraits.join(', ') : undefined; }