1872 lines
80 KiB
TypeScript
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>
|
|
);
|
|
}
|