import { useState, useEffect } from 'react'; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Sliders, Plus, Users, Save, Loader, Trash2 } from 'lucide-react'; import { toastService } from '@/lib/toast'; import { personasApi, authApi } from '@/lib/api'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '@/contexts/AuthContext'; interface UserCreatorProps { targetFolderId?: string | null; targetFolderName?: string | null; } import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent } from "@/components/ui/card"; const formSchema = z.object({ // Basic Information name: z.string().min(2, { message: "Name must be at least 2 characters.", }), age: z.string().min(1, { message: "Age is required.", }), gender: z.string().min(1, { message: "Gender is required.", }), occupation: z.string().min(2, { message: "Occupation is required.", }), education: z.string().min(1, { message: "Education is required.", }), location: z.string().min(2, { message: "Location is required.", }), ethnicity: z.string().optional(), personality: z.string(), interests: z.string(), hasPurchasingPower: z.boolean().optional(), hasChildren: z.boolean().optional(), // Behavioral Attributes techSavviness: z.number().min(0).max(100), brandLoyalty: z.number().min(0).max(100), priceConsciousness: z.number().min(0).max(100), environmentalConcern: z.number().min(0).max(100), // Demographics socialGrade: z.string().optional(), householdIncome: z.string().optional(), householdComposition: z.string().optional(), livingSituation: z.string().optional(), // Cooper Profile goals: z.array(z.string()).optional(), frustrations: z.array(z.string()).optional(), motivations: z.array(z.string()).optional(), scenarios: z.array(z.string()).optional(), scenarioType: z.string().optional(), // OCEAN Personality Traits oceanTraits: z.object({ openness: z.number().min(0).max(100), conscientiousness: z.number().min(0).max(100), extraversion: z.number().min(0).max(100), agreeableness: z.number().min(0).max(100), neuroticism: z.number().min(0).max(100), }).optional(), // Think Feel Do thinkFeelDo: z.object({ thinks: z.array(z.string()), feels: z.array(z.string()), does: z.array(z.string()), }).optional(), // Lifestyle & Behavior mediaConsumption: z.string().optional(), deviceUsage: z.string().optional(), shoppingHabits: z.string().optional(), brandPreferences: z.string().optional(), communicationPreferences: z.string().optional(), paymentMethods: z.string().optional(), purchaseBehaviour: z.string().optional(), // Extended Profile coreValues: z.string().optional(), lifestyleChoices: z.string().optional(), socialActivities: z.string().optional(), categoryKnowledge: z.string().optional(), decisionInfluences: z.string().optional(), painPoints: z.string().optional(), journeyContext: z.string().optional(), keyTouchpoints: z.string().optional(), selfDeterminationNeeds: z.object({ autonomy: z.string(), competence: z.string(), relatedness: z.string(), }).optional(), fears: z.array(z.string()).optional(), narrative: z.string().optional(), additionalInformation: z.string().optional(), }); export default function UserCreator({ targetFolderId, targetFolderName }: UserCreatorProps) { const [userCount, setUserCount] = useState(1); const [isSubmitting, setIsSubmitting] = useState(false); const [isAutoLoginInProgress, setIsAutoLoginInProgress] = useState(false); const [retryCount, setRetryCount] = useState(0); const navigate = useNavigate(); const { isAuthenticated, login } = useAuth(); // Reset retry count when component mounts useEffect(() => { setRetryCount(0); }, []); // Perform automatic login if needed (only once on initial load) useEffect(() => { const attemptAutoLogin = async () => { // Only try auto-login if not already authenticated and not already in progress if (!isAuthenticated && !isAutoLoginInProgress) { setIsAutoLoginInProgress(true); try { console.log('Attempting auto login with default credentials'); await login('user', 'pass'); console.log('Auto login successful'); // Verify the token was stored const storedToken = localStorage.getItem('auth_token'); if (storedToken) { console.log('Token successfully stored:', storedToken.substring(0, 10) + '...'); toastService.success('Logged in automatically with default account'); } else { console.error('Token not stored after successful login'); toastService.error('Authentication problem, token not stored'); } } catch (error) { console.error('Auto login failed:', error); // Don't show a toast here - we'll handle login failures silently } finally { setIsAutoLoginInProgress(false); } } }; attemptAutoLogin(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only run once on component mount const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { // Basic Information name: "", age: "", gender: "", occupation: "", education: "", location: "", ethnicity: "", personality: "", interests: "", hasPurchasingPower: false, hasChildren: false, // Behavioral Attributes techSavviness: 50, brandLoyalty: 50, priceConsciousness: 50, environmentalConcern: 50, // Demographics socialGrade: "", householdIncome: "", householdComposition: "", livingSituation: "", // Cooper Profile goals: [], frustrations: [], motivations: [], scenarios: [], scenarioType: "", // OCEAN Personality Traits oceanTraits: { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50, }, // Think Feel Do thinkFeelDo: { thinks: [], feels: [], does: [], }, // Lifestyle & Behavior mediaConsumption: "", deviceUsage: "", shoppingHabits: "", brandPreferences: "", communicationPreferences: "", paymentMethods: "", purchaseBehaviour: "", // Extended Profile coreValues: "", lifestyleChoices: "", socialActivities: "", categoryKnowledge: "", decisionInfluences: "", painPoints: "", journeyContext: "", keyTouchpoints: "", selfDeterminationNeeds: { autonomy: "", competence: "", relatedness: "", }, fears: [], narrative: "", additionalInformation: "", }, }); // Helper functions for managing array fields const handleArrayAdd = (field: 'goals' | 'frustrations' | 'motivations' | 'scenarios' | 'fears') => { const currentValue = form.getValues(field) || []; form.setValue(field, [...currentValue, '']); }; const handleArrayUpdate = (field: 'goals' | 'frustrations' | 'motivations' | 'scenarios' | 'fears', index: number, value: string) => { const currentValue = form.getValues(field) || []; const newArray = [...currentValue]; newArray[index] = value; form.setValue(field, newArray); }; const handleArrayRemove = (field: 'goals' | 'frustrations' | 'motivations' | 'scenarios' | 'fears', index: number) => { const currentValue = form.getValues(field) || []; const newArray = [...currentValue]; newArray.splice(index, 1); form.setValue(field, newArray); }; const handleThinkFeelDoAdd = (category: 'thinks' | 'feels' | 'does') => { const currentValue = form.getValues('thinkFeelDo') || { thinks: [], feels: [], does: [] }; const newValue = { ...currentValue, [category]: [...(currentValue[category] || []), ''] }; form.setValue('thinkFeelDo', newValue); }; const handleThinkFeelDoUpdate = (category: 'thinks' | 'feels' | 'does', index: number, value: string) => { const currentValue = form.getValues('thinkFeelDo') || { thinks: [], feels: [], does: [] }; const newArray = [...(currentValue[category] || [])]; newArray[index] = value; const newValue = { ...currentValue, [category]: newArray }; form.setValue('thinkFeelDo', newValue); }; const handleThinkFeelDoRemove = (category: 'thinks' | 'feels' | 'does', index: number) => { const currentValue = form.getValues('thinkFeelDo') || { thinks: [], feels: [], does: [] }; const newArray = [...(currentValue[category] || [])]; newArray.splice(index, 1); const newValue = { ...currentValue, [category]: newArray }; form.setValue('thinkFeelDo', newValue); }; const handleOceanChange = (trait: keyof typeof form.getValues['oceanTraits'], value: number) => { const currentValue = form.getValues('oceanTraits') || { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50, }; const newValue = { ...currentValue, [trait]: value }; form.setValue('oceanTraits', newValue); }; async function onSubmit(values: z.infer, isRetry = false) { // Prevent infinite loops by limiting retries if (isRetry && retryCount >= 1) { console.log('Max retry attempts reached, stopping retry loop'); toastService.error("Authentication failed after multiple attempts", { description: "Please try logging in manually (user/pass)" }); navigate('/login', { state: { from: '/synthetic-users' } }); setIsSubmitting(false); return; } if (isRetry) { setRetryCount(prevCount => prevCount + 1); console.log(`Retry attempt ${retryCount + 1}`); } else { setRetryCount(0); // Reset retry count for new submissions } setIsSubmitting(true); try { // If not authenticated, try to log in with default credentials if (!isAuthenticated) { try { console.log('Not authenticated, attempting login with default credentials before submission'); await login('user', 'pass'); console.log('Login successful before persona creation'); } catch (loginError) { console.error('Login failed before persona creation:', loginError); toastService.error("Authentication required", { description: "Please log in before creating personas. Default: user/pass" }); navigate('/login', { state: { from: '/synthetic-users' } }); setIsSubmitting(false); return; } } // Create persistent toast for persona generation process const generationToastId = `persona-generation-${Date.now()}`; const folderText = targetFolderId && targetFolderName ? ` in "${targetFolderName}" folder` : ''; const personaText = userCount > 1 ? `${userCount} personas` : 'persona'; console.log(`UserCreator - Creating ${personaText}${folderText}`); toastService.createPersistent({ id: generationToastId, title: `Generating ${personaText}...`, description: `Creating synthetic user profile${userCount > 1 ? 's' : ''}${folderText}`, type: 'info' }); // Create the base persona const personaData = { ...values, // Ensure oceanTraits and thinkFeelDo are properly set from form values oceanTraits: values.oceanTraits || { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50 }, thinkFeelDo: values.thinkFeelDo || { thinks: [], feels: [], does: [] }, // Add folder ID if provided folderId: targetFolderId || undefined }; // Store the persona temporarily in localStorage as a fallback const tempPersonaData = { id: `temp-${Date.now()}`, ...personaData }; const storedPersonas = JSON.parse(localStorage.getItem('tempPersonas') || '[]'); storedPersonas.push(tempPersonaData); localStorage.setItem('tempPersonas', JSON.stringify(storedPersonas)); // Try to save to the database via API if (userCount === 1) { // Just create a single persona try { // Check token to ensure we're authenticated const token = localStorage.getItem('auth_token'); if (!token) { console.error("No authentication token found"); toastService.error("Authentication required", { description: "No valid token found. Please log in again." }); // Force a new login try { console.log('No token found, attempting new login'); await login('user', 'pass'); console.log('Login successful, token:', localStorage.getItem('auth_token')?.substring(0, 10) + '...'); } catch (loginError) { console.error('Login retry failed:', loginError); throw new Error('Authentication failed after retry'); } } console.log("Sending persona creation request to API with auth token"); const result = await personasApi.create(personaData); console.log("Persona created successfully:", result); // Update persistent toast with success toastService.updatePersistent(generationToastId, { title: "Synthetic user created successfully", description: `Created profile for ${values.name}`, type: 'success' }); } catch (apiError) { console.error("Error creating persona via API:", apiError); // If we get a 401 error, try to re-authenticate if (apiError.response && apiError.response.status === 401) { toastService.error("Authentication error", { description: "Failed to authenticate with server. Please try again." }); throw apiError; // Rethrow to handle in the catch block above } else { // For other errors, throw to handle in the catch block above throw apiError; } } } else { // Create multiple personas with variations const batch = []; // First add the exact persona batch.push(personaData); // Then add variations for (let i = 1; i < userCount; i++) { const variation = { ...personaData, name: `${values.name} (Variation ${i})`, techSavviness: Math.max(0, Math.min(100, values.techSavviness + Math.floor(Math.random() * 30) - 15)), brandLoyalty: Math.max(0, Math.min(100, values.brandLoyalty + Math.floor(Math.random() * 30) - 15)), priceConsciousness: Math.max(0, Math.min(100, values.priceConsciousness + Math.floor(Math.random() * 30) - 15)), environmentalConcern: Math.max(0, Math.min(100, values.environmentalConcern + Math.floor(Math.random() * 30) - 15)), // Keep the folder ID for each variation folderId: targetFolderId || undefined }; batch.push(variation); } try { // Check token to ensure we're authenticated const token = localStorage.getItem('auth_token'); if (!token) { console.error("No authentication token found"); toastService.error("Authentication required", { description: "No valid token found. Please log in again." }); // Force a new login try { console.log('No token found, attempting new login'); await login('user', 'pass'); console.log('Login successful, token:', localStorage.getItem('auth_token')?.substring(0, 10) + '...'); } catch (loginError) { console.error('Login retry failed:', loginError); throw new Error('Authentication failed after retry'); } } console.log("Sending batch persona creation request to API with auth token"); const result = await personasApi.createBatch(batch); console.log("Batch personas created successfully:", result); // Update persistent toast with success toastService.updatePersistent(generationToastId, { title: `${userCount} synthetic users created successfully`, description: `Created profile for ${values.name} and ${userCount - 1} variations`, type: 'success' }); } catch (apiError) { console.error("Error creating batch personas via API:", apiError); // If we get a 401 error, try to re-authenticate if (apiError.response && apiError.response.status === 401) { toastService.error("Authentication error", { description: "Failed to authenticate with server. Please try again." }); throw apiError; // Rethrow to handle in the catch block above } else { // For other errors, throw to handle in the catch block above throw apiError; } } } console.log("Persona creation successful (database storage)"); form.reset(); // Remove temp data since we succeeded localStorage.removeItem('tempPersonas'); // Navigate back to the synthetic users page after successful creation setTimeout(() => { navigate('/synthetic-users?mode=view'); }, 300); } catch (error: unknown) { console.error("Error creating personas:", error); // Handle authentication errors specifically if ((error.response && error.response.status === 401) || (error.message && error.message.includes('Authentication failed')) && retryCount < 1) { // Try to log in again with default credentials try { console.log('Got auth error, attempting login retry with default credentials'); // Force clear any existing tokens that might be invalid localStorage.removeItem('auth_token'); const response = await authApi.login('user', 'pass'); if (response?.data?.access_token) { localStorage.setItem('auth_token', response.data.access_token); localStorage.setItem('user', JSON.stringify(response.data.user)); console.log('Manual login successful, got new token:', response.data.access_token.substring(0, 10) + '...'); // If login succeeds, retry submission with retry flag toastService.info("Logged in with default account, retrying submission..."); // Use setTimeout to avoid potential call stack issues setTimeout(() => { onSubmit(values, true); // Retry submission with flag }, 500); return; } else { throw new Error('No access token received'); } } catch (loginError) { console.error('Login retry failed:', loginError); toastService.error("Authentication error", { description: "Cannot authenticate with server. Please contact support." }); } } else { // If not an auth error or we've already retried, show general error // Update persistent toast with error toastService.updatePersistent(generationToastId, { title: "Failed to create synthetic users", description: error.response?.data?.message || error.message || "An unexpected error occurred", type: 'error' }); } } finally { setIsSubmitting(false); } } return (

Create Synthetic Users

{userCount}
Basic Cooper Personality Demographics Lifestyle Extended
( Name )} />
( Age Range )} /> ( Gender )} />
( Occupation )} /> ( Education )} /> ( Location )} /> ( Ethnicity (Optional) )} />
( Personality Traits