semblance-dev/src/components/UserCreator.tsx

1872 lines
80 KiB
TypeScript

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<z.infer<typeof formSchema>>({
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<typeof formSchema>, 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 (
<div className="glass-panel rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-sf font-semibold">Create Synthetic Users</h2>
<div className="flex items-center space-x-2">
<Button variant="outline" size="sm" onClick={() => setUserCount(Math.max(1, userCount - 1))}>-</Button>
<div className="flex items-center space-x-2">
<Users size={16} className="text-muted-foreground" />
<span className="text-sm font-medium">{userCount}</span>
</div>
<Button variant="outline" size="sm" onClick={() => setUserCount(userCount + 1)}>+</Button>
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Tabs defaultValue="basic">
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="cooper">Cooper</TabsTrigger>
<TabsTrigger value="personality">Personality</TabsTrigger>
<TabsTrigger value="demographics">Demographics</TabsTrigger>
<TabsTrigger value="lifestyle">Lifestyle</TabsTrigger>
<TabsTrigger value="extended">Extended</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="mt-6">
<Card>
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Jane Smith" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="age"
render={({ field }) => (
<FormItem>
<FormLabel>Age Range</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select age range" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="18-24">18-24</SelectItem>
<SelectItem value="25-34">25-34</SelectItem>
<SelectItem value="35-44">35-44</SelectItem>
<SelectItem value="45-54">45-54</SelectItem>
<SelectItem value="55-64">55-64</SelectItem>
<SelectItem value="65+">65+</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gender"
render={({ field }) => (
<FormItem>
<FormLabel>Gender</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select gender" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Male">Male</SelectItem>
<SelectItem value="Female">Female</SelectItem>
<SelectItem value="Non-binary">Non-binary</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="occupation"
render={({ field }) => (
<FormItem>
<FormLabel>Occupation</FormLabel>
<FormControl>
<Input placeholder="Software Engineer" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="education"
render={({ field }) => (
<FormItem>
<FormLabel>Education</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select education level" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="High School">High School</SelectItem>
<SelectItem value="Some College">Some College</SelectItem>
<SelectItem value="Associate's Degree">Associate's Degree</SelectItem>
<SelectItem value="Bachelor's Degree">Bachelor's Degree</SelectItem>
<SelectItem value="Master's Degree">Master's Degree</SelectItem>
<SelectItem value="PhD">PhD</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="location"
render={({ field }) => (
<FormItem>
<FormLabel>Location</FormLabel>
<FormControl>
<Input placeholder="New York, USA" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="ethnicity"
render={({ field }) => (
<FormItem>
<FormLabel>Ethnicity (Optional)</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select ethnicity" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="white">White</SelectItem>
<SelectItem value="black">Black</SelectItem>
<SelectItem value="asian">Asian</SelectItem>
<SelectItem value="hispanic">Hispanic/Latino</SelectItem>
<SelectItem value="native-american">Native American</SelectItem>
<SelectItem value="middle-eastern">Middle Eastern</SelectItem>
<SelectItem value="mixed">Mixed</SelectItem>
<SelectItem value="other">Other</SelectItem>
<SelectItem value="prefer-not-to-say">Prefer not to say</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="personality"
render={({ field }) => (
<FormItem>
<FormLabel>Personality Traits</FormLabel>
<FormControl>
<Textarea placeholder="Curious, analytical, detail-oriented" {...field} rows={3} />
</FormControl>
<FormDescription>
Describe key personality traits that define this user
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="interests"
render={({ field }) => (
<FormItem>
<FormLabel>Interests</FormLabel>
<FormControl>
<Textarea placeholder="Technology, fitness, cooking, travel" {...field} rows={3} />
</FormControl>
<FormDescription>
List interests, hobbies and activities this user enjoys
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<h3 className="font-medium text-sm">Behavioral Attributes</h3>
<FormField
control={form.control}
name="techSavviness"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between mb-2">
<FormLabel>Tech Savviness</FormLabel>
<span className="text-sm text-muted-foreground">{field.value}%</span>
</div>
<FormControl>
<Slider
min={0}
max={100}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="brandLoyalty"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between mb-2">
<FormLabel>Brand Loyalty</FormLabel>
<span className="text-sm text-muted-foreground">{field.value}%</span>
</div>
<FormControl>
<Slider
min={0}
max={100}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priceConsciousness"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between mb-2">
<FormLabel>Price Consciousness</FormLabel>
<span className="text-sm text-muted-foreground">{field.value}%</span>
</div>
<FormControl>
<Slider
min={0}
max={100}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="environmentalConcern"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between mb-2">
<FormLabel>Environmental Concern</FormLabel>
<span className="text-sm text-muted-foreground">{field.value}%</span>
</div>
<FormControl>
<Slider
min={0}
max={100}
step={1}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4 pt-2">
<FormField
control={form.control}
name="hasPurchasingPower"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<FormLabel>Purchasing Power</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="hasChildren"
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<FormLabel>Has Children</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="cooper" className="mt-6 space-y-6">
<Card>
<CardContent className="p-6">
<div className="mb-4">
<h3 className="font-medium text-lg mb-3">Goals</h3>
{(form.watch('goals') || []).map((goal, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={goal}
onChange={e => handleArrayUpdate('goals', index, e.target.value)}
placeholder="Enter a goal"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleArrayRemove('goals', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleArrayAdd('goals')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Goal
</Button>
</div>
<div className="mb-4 pt-4 border-t">
<h3 className="font-medium text-lg mb-3">Frustrations</h3>
{(form.watch('frustrations') || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleArrayUpdate('frustrations', index, e.target.value)}
placeholder="Enter a frustration"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleArrayRemove('frustrations', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleArrayAdd('frustrations')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Frustration
</Button>
</div>
<div className="mb-4 pt-4 border-t">
<h3 className="font-medium text-lg mb-3">Motivations</h3>
{(form.watch('motivations') || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleArrayUpdate('motivations', index, e.target.value)}
placeholder="Enter a motivation"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleArrayRemove('motivations', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleArrayAdd('motivations')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Motivation
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<h3 className="font-medium text-lg mb-4">Think, Feel, Do</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<h4 className="font-medium text-sm mb-3">Thinks</h4>
{((form.watch('thinkFeelDo') || { thinks: [], feels: [], does: [] }).thinks || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleThinkFeelDoUpdate('thinks', index, e.target.value)}
placeholder="What they think"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleThinkFeelDoRemove('thinks', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleThinkFeelDoAdd('thinks')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Thought
</Button>
</div>
<div>
<h4 className="font-medium text-sm mb-3">Feels</h4>
{((form.watch('thinkFeelDo') || { thinks: [], feels: [], does: [] }).feels || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleThinkFeelDoUpdate('feels', index, e.target.value)}
placeholder="What they feel"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleThinkFeelDoRemove('feels', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleThinkFeelDoAdd('feels')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Feeling
</Button>
</div>
<div>
<h4 className="font-medium text-sm mb-3">Does</h4>
{((form.watch('thinkFeelDo') || { thinks: [], feels: [], does: [] }).does || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleThinkFeelDoUpdate('does', index, e.target.value)}
placeholder="What they do"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleThinkFeelDoRemove('does', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleThinkFeelDoAdd('does')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Action
</Button>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<FormField
control={form.control}
name="scenarioType"
render={({ field }) => (
<FormItem>
<FormLabel>Scenario Section Title</FormLabel>
<FormControl>
<Input placeholder="Life Scenarios" {...field} />
</FormControl>
<FormDescription>
Custom title for the scenarios section (e.g., "Customer Journey", "Use Cases")
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div>
<h3 className="font-medium text-lg mb-3">Usage Scenarios</h3>
{(form.watch('scenarios') || []).map((item, index) => (
<div key={index} className="flex items-start gap-2 mb-2">
<Textarea
value={item}
onChange={e => handleArrayUpdate('scenarios', index, e.target.value)}
rows={2}
placeholder="Describe a usage scenario"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleArrayRemove('scenarios', index)}
className="mt-2"
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleArrayAdd('scenarios')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Scenario
</Button>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="personality" className="mt-6">
<Card>
<CardContent className="p-6">
<h3 className="font-medium text-lg mb-4">OCEAN Personality Traits</h3>
<div className="space-y-6">
<div>
<div className="flex justify-between mb-1">
<label className="text-sm font-medium">Openness to Experience</label>
<span className="text-sm">{(form.watch('oceanTraits') || { openness: 50 }).openness || 50}%</span>
</div>
<Slider
value={[(form.watch('oceanTraits') || { openness: 50 }).openness || 50]}
onValueChange={values => handleOceanChange('openness', values[0])}
max={100}
step={1}
/>
<p className="text-xs text-muted-foreground mt-1">
Creativity, curiosity, and openness to new ideas
</p>
</div>
<div>
<div className="flex justify-between mb-1">
<label className="text-sm font-medium">Conscientiousness</label>
<span className="text-sm">{(form.watch('oceanTraits') || { conscientiousness: 50 }).conscientiousness || 50}%</span>
</div>
<Slider
value={[(form.watch('oceanTraits') || { conscientiousness: 50 }).conscientiousness || 50]}
onValueChange={values => handleOceanChange('conscientiousness', values[0])}
max={100}
step={1}
/>
<p className="text-xs text-muted-foreground mt-1">
Organization, responsibility, and self-discipline
</p>
</div>
<div>
<div className="flex justify-between mb-1">
<label className="text-sm font-medium">Extraversion</label>
<span className="text-sm">{(form.watch('oceanTraits') || { extraversion: 50 }).extraversion || 50}%</span>
</div>
<Slider
value={[(form.watch('oceanTraits') || { extraversion: 50 }).extraversion || 50]}
onValueChange={values => handleOceanChange('extraversion', values[0])}
max={100}
step={1}
/>
<p className="text-xs text-muted-foreground mt-1">
Sociability, assertiveness, and talkativeness
</p>
</div>
<div>
<div className="flex justify-between mb-1">
<label className="text-sm font-medium">Agreeableness</label>
<span className="text-sm">{(form.watch('oceanTraits') || { agreeableness: 50 }).agreeableness || 50}%</span>
</div>
<Slider
value={[(form.watch('oceanTraits') || { agreeableness: 50 }).agreeableness || 50]}
onValueChange={values => handleOceanChange('agreeableness', values[0])}
max={100}
step={1}
/>
<p className="text-xs text-muted-foreground mt-1">
Compassion, cooperation, and concern for others
</p>
</div>
<div>
<div className="flex justify-between mb-1">
<label className="text-sm font-medium">Neuroticism</label>
<span className="text-sm">{(form.watch('oceanTraits') || { neuroticism: 50 }).neuroticism || 50}%</span>
</div>
<Slider
value={[(form.watch('oceanTraits') || { neuroticism: 50 }).neuroticism || 50]}
onValueChange={values => handleOceanChange('neuroticism', values[0])}
max={100}
step={1}
/>
<p className="text-xs text-muted-foreground mt-1">
Emotional reactivity, anxiety, and sensitivity to stress
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="demographics" className="mt-6">
<Card>
<CardContent className="p-6">
<h3 className="font-medium text-lg mb-4">Demographic Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<FormField
control={form.control}
name="socialGrade"
render={({ field }) => (
<FormItem>
<FormLabel>Social Grade</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select social grade" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="A">A - Higher managerial</SelectItem>
<SelectItem value="B">B - Intermediate managerial</SelectItem>
<SelectItem value="C1">C1 - Supervisory or clerical</SelectItem>
<SelectItem value="C2">C2 - Skilled manual workers</SelectItem>
<SelectItem value="D">D - Semi and unskilled manual workers</SelectItem>
<SelectItem value="E">E - State pensioners, unemployed</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="householdIncome"
render={({ field }) => (
<FormItem>
<FormLabel>Household Income</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select income range" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Under $25k">Under $25,000</SelectItem>
<SelectItem value="$25k-$50k">$25,000 - $50,000</SelectItem>
<SelectItem value="$50k-$75k">$50,000 - $75,000</SelectItem>
<SelectItem value="$75k-$100k">$75,000 - $100,000</SelectItem>
<SelectItem value="$100k-$150k">$100,000 - $150,000</SelectItem>
<SelectItem value="$150k-$250k">$150,000 - $250,000</SelectItem>
<SelectItem value="Over $250k">Over $250,000</SelectItem>
<SelectItem value="Prefer not to say">Prefer not to say</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="householdComposition"
render={({ field }) => (
<FormItem>
<FormLabel>Household Composition</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select household type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Single person">Single person</SelectItem>
<SelectItem value="Couple without children">Couple without children</SelectItem>
<SelectItem value="Couple with children">Couple with children</SelectItem>
<SelectItem value="Single parent">Single parent</SelectItem>
<SelectItem value="Multi-generational">Multi-generational</SelectItem>
<SelectItem value="Shared housing">Shared housing</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="livingSituation"
render={({ field }) => (
<FormItem>
<FormLabel>Living Situation</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select living situation" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Own home">Own home</SelectItem>
<SelectItem value="Rent apartment">Rent apartment</SelectItem>
<SelectItem value="Rent house">Rent house</SelectItem>
<SelectItem value="Live with family">Live with family</SelectItem>
<SelectItem value="Student housing">Student housing</SelectItem>
<SelectItem value="Assisted living">Assisted living</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="lifestyle" className="mt-6">
<Card>
<CardContent className="p-6">
<h3 className="font-medium text-lg mb-4">Lifestyle & Behavior</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<FormField
control={form.control}
name="mediaConsumption"
render={({ field }) => (
<FormItem>
<FormLabel>Media Consumption</FormLabel>
<FormControl>
<Textarea
placeholder="TV shows, podcasts, news sources, social media platforms"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Describe media consumption habits and preferences
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="deviceUsage"
render={({ field }) => (
<FormItem>
<FormLabel>Device Usage</FormLabel>
<FormControl>
<Textarea
placeholder="Smartphone, laptop, tablet, smart TV, gaming console"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Primary devices and usage patterns
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="shoppingHabits"
render={({ field }) => (
<FormItem>
<FormLabel>Shopping Habits</FormLabel>
<FormControl>
<Textarea
placeholder="Online vs in-store, frequency, preferred retailers"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Shopping behavior and preferences
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="brandPreferences"
render={({ field }) => (
<FormItem>
<FormLabel>Brand Preferences</FormLabel>
<FormControl>
<Textarea
placeholder="Favorite brands, brand values alignment"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Preferred brands and reasoning
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="communicationPreferences"
render={({ field }) => (
<FormItem>
<FormLabel>Communication Preferences</FormLabel>
<FormControl>
<Textarea
placeholder="Email, phone, text, video calls, in-person"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Preferred communication methods and channels
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="paymentMethods"
render={({ field }) => (
<FormItem>
<FormLabel>Payment Methods</FormLabel>
<FormControl>
<Textarea
placeholder="Credit cards, digital wallets, cash, BNPL"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
Preferred payment methods and financial tools
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="purchaseBehaviour"
render={({ field }) => (
<FormItem>
<FormLabel>Purchase Behavior</FormLabel>
<FormControl>
<Textarea
placeholder="Research habits, decision factors, impulse vs planned buying"
{...field}
rows={3}
/>
</FormControl>
<FormDescription>
How they approach making purchase decisions
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="extended" className="mt-6 space-y-6">
<Card>
<CardContent className="p-6">
<h3 className="font-medium text-lg mb-4">Extended Profile</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4">
<FormField
control={form.control}
name="coreValues"
render={({ field }) => (
<FormItem>
<FormLabel>Core Values</FormLabel>
<FormControl>
<Textarea
placeholder="Key principles and values that guide decisions"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lifestyleChoices"
render={({ field }) => (
<FormItem>
<FormLabel>Lifestyle Choices</FormLabel>
<FormControl>
<Textarea
placeholder="Health, fitness, diet, work-life balance preferences"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="socialActivities"
render={({ field }) => (
<FormItem>
<FormLabel>Social Activities</FormLabel>
<FormControl>
<Textarea
placeholder="Social hobbies, community involvement, networking"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="categoryKnowledge"
render={({ field }) => (
<FormItem>
<FormLabel>Category Knowledge</FormLabel>
<FormControl>
<Textarea
placeholder="Expertise in specific product/service categories"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="decisionInfluences"
render={({ field }) => (
<FormItem>
<FormLabel>Decision Influences</FormLabel>
<FormControl>
<Textarea
placeholder="What factors most influence their decisions"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="painPoints"
render={({ field }) => (
<FormItem>
<FormLabel>Pain Points</FormLabel>
<FormControl>
<Textarea
placeholder="Common challenges and friction points"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="space-y-4">
<FormField
control={form.control}
name="journeyContext"
render={({ field }) => (
<FormItem>
<FormLabel>Journey Context</FormLabel>
<FormControl>
<Textarea
placeholder="Current life stage and contextual factors"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="keyTouchpoints"
render={({ field }) => (
<FormItem>
<FormLabel>Key Touchpoints</FormLabel>
<FormControl>
<Textarea
placeholder="Important interaction points and channels"
{...field}
rows={3}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<h4 className="font-medium text-sm">Self-Determination Needs</h4>
<FormField
control={form.control}
name="selfDeterminationNeeds.autonomy"
render={({ field }) => (
<FormItem>
<FormLabel>Autonomy</FormLabel>
<FormControl>
<Textarea
placeholder="Need for independence and self-direction"
{...field}
rows={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="selfDeterminationNeeds.competence"
render={({ field }) => (
<FormItem>
<FormLabel>Competence</FormLabel>
<FormControl>
<Textarea
placeholder="Need to feel capable and effective"
{...field}
rows={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="selfDeterminationNeeds.relatedness"
render={({ field }) => (
<FormItem>
<FormLabel>Relatedness</FormLabel>
<FormControl>
<Textarea
placeholder="Need for connection and belonging"
{...field}
rows={2}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<h3 className="font-medium text-lg mb-3">Fears & Concerns</h3>
{(form.watch('fears') || []).map((item, index) => (
<div key={index} className="flex items-center gap-2 mb-2">
<Input
value={item}
onChange={e => handleArrayUpdate('fears', index, e.target.value)}
placeholder="Enter a fear or concern"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleArrayRemove('fears', index)}
>
<Trash2 className="h-4 w-4 text-muted-foreground" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
type="button"
onClick={() => handleArrayAdd('fears')}
className="mt-2"
>
<Plus className="h-4 w-4 mr-2" />
Add Fear/Concern
</Button>
</div>
<FormField
control={form.control}
name="narrative"
render={({ field }) => (
<FormItem>
<FormLabel>Personal Narrative</FormLabel>
<FormControl>
<Textarea
placeholder="Personal story, background, key life experiences"
{...field}
rows={4}
/>
</FormControl>
<FormDescription>
A brief narrative that captures their personal story
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="additionalInformation"
render={({ field }) => (
<FormItem>
<FormLabel>Additional Information</FormLabel>
<FormControl>
<Textarea
placeholder="Any other relevant details or context"
{...field}
rows={4}
/>
</FormControl>
<FormDescription>
Additional context or details not covered elsewhere
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2">
<Button variant="outline" type="button" onClick={() => form.reset()}>
Reset
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<Loader className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
{isSubmitting ? 'Creating...' : `Create ${userCount > 1 ? `${userCount} Users` : 'User'}`}
</Button>
</div>
</form>
</Form>
</div>
);
}