232 lines
No EOL
9.6 KiB
TypeScript
232 lines
No EOL
9.6 KiB
TypeScript
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<Persona[]> {
|
|
// 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;
|
|
} |