feat: form redesign — Synthetic Users (Epic 3)

AI Recruiter form:
- Word-count indicator floats inside textarea corner (absolute overlay)
- Model dropdown: compact label "Model", removed description, shorter option text
- personaCount: removed description, label → "Number of personas"

Manual Creation form:
- Behavioral Attributes (4): Slider → Input[type=number] inline with label+% suffix
- OCEAN traits (5): Slider → Input[type=number] with description below
- userCount header: +/- buttons → single Input[type=number] (w-16, centered)
- TabsList: sticky top-0 z-10 so tabs stay visible while scrolling
- Removed unused Slider and Sliders imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-24 14:18:32 +01:00
parent d679691cc3
commit c8d4591117
2 changed files with 109 additions and 208 deletions

View file

@ -2,7 +2,7 @@ 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 { 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';
@ -31,7 +31,6 @@ import {
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";
@ -559,20 +558,24 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
<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 className="flex items-center gap-2">
<Users size={15} className="text-muted-foreground" />
<Input
type="number"
min={1}
max={10}
value={userCount}
onChange={e => setUserCount(Math.min(10, Math.max(1, parseInt(e.target.value) || 1)))}
className="w-16 h-8 text-center text-sm"
/>
<span className="text-xs text-muted-foreground">users</span>
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Tabs defaultValue="basic">
<TabsList className="grid w-full grid-cols-6">
<TabsList className="grid w-full grid-cols-6 sticky top-0 z-10 bg-card">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="attitudinal">Attitudinal</TabsTrigger>
<TabsTrigger value="personality">Personality</TabsTrigger>
@ -771,100 +774,44 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
)}
/>
<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="space-y-3">
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">Behavioral Attributes</h3>
{(
[
{ name: 'techSavviness', label: 'Tech Savviness' },
{ name: 'brandLoyalty', label: 'Brand Loyalty' },
{ name: 'priceConsciousness', label: 'Price Consciousness' },
{ name: 'environmentalConcern', label: 'Environmental Concern' },
] as const
).map(({ name, label }) => (
<FormField
key={name}
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between gap-3">
<FormLabel className="flex-1 text-sm font-medium">{label}</FormLabel>
<div className="flex items-center gap-1.5 flex-shrink-0">
<FormControl>
<Input
type="number"
min={0}
max={100}
value={field.value}
onChange={e => field.onChange(Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))}
className="w-16 h-8 text-center text-sm"
/>
</FormControl>
<span className="text-xs text-muted-foreground w-4">%</span>
</div>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
<div className="grid grid-cols-2 gap-4 pt-2">
<FormField
@ -1168,87 +1115,39 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
<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 className="space-y-4">
{(
[
{ key: 'openness', label: 'Openness to Experience', desc: 'Creativity, curiosity, and openness to new ideas' },
{ key: 'conscientiousness', label: 'Conscientiousness', desc: 'Organization, responsibility, and self-discipline' },
{ key: 'extraversion', label: 'Extraversion', desc: 'Sociability, assertiveness, and talkativeness' },
{ key: 'agreeableness', label: 'Agreeableness', desc: 'Compassion, cooperation, and concern for others' },
{ key: 'neuroticism', label: 'Neuroticism', desc: 'Emotional reactivity, anxiety, and sensitivity to stress' },
] as const
).map(({ key, label, desc }) => {
const ocean = form.watch('oceanTraits') || { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50 };
const val = ocean[key] ?? 50;
return (
<div key={key} className="space-y-1">
<div className="flex items-center justify-between gap-3">
<label className="text-sm font-medium flex-1">{label}</label>
<div className="flex items-center gap-1.5 flex-shrink-0">
<Input
type="number"
min={0}
max={100}
value={val}
onChange={e => handleOceanChange(key, Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))}
className="w-16 h-8 text-center text-sm"
/>
<span className="text-xs text-muted-foreground w-4">%</span>
</div>
</div>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
);
})}
</div>
</CardContent>
</Card>

View file

@ -179,13 +179,17 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
/>
</FormLabel>
<FormControl>
<Textarea
placeholder="Describe your target audience(s). Who you want to talk to, their demographics and attitudes..."
className="h-40"
{...field}
/>
<div className="relative">
<Textarea
placeholder="Describe your target audience(s). Who you want to talk to, their demographics and attitudes..."
className="h-40 pb-8"
{...field}
/>
<div className="absolute bottom-2 left-3 pointer-events-none">
<InputStrengthIndicator text={field.value || ""} minWords={150} />
</div>
</div>
</FormControl>
<InputStrengthIndicator text={field.value || ""} minWords={150} />
<FormMessage />
</FormItem>
)}
@ -204,13 +208,17 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
/>
</FormLabel>
<FormControl>
<Textarea
placeholder="Describe what you want to learn. What are the main research topics or areas of interest you want to explore? For instance, 'Understanding switching behaviours in TV markets' or 'Learning about Millennial attitudes towards Facebook ads'..."
className="h-32"
{...field}
/>
<div className="relative">
<Textarea
placeholder="Describe what you want to learn. What are the main research topics or areas of interest you want to explore? For instance, 'Understanding switching behaviours in TV markets' or 'Learning about Millennial attitudes towards Facebook ads'..."
className="h-32 pb-8"
{...field}
/>
<div className="absolute bottom-2 left-3 pointer-events-none">
<InputStrengthIndicator text={field.value || ""} minWords={150} />
</div>
</div>
</FormControl>
<InputStrengthIndicator text={field.value || ""} minWords={150} />
<FormMessage />
</FormItem>
)}
@ -338,39 +346,33 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
name="llm_model"
render={({ field }) => (
<FormItem>
<FormLabel>AI Model</FormLabel>
<FormLabel>Model</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select AI model" />
<SelectValue placeholder="Select model" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="gpt-5.4">GPT-5.4 (Recommended)</SelectItem>
<SelectItem value="gpt-5.4-mini">GPT-5.4 Mini (Faster, lower cost)</SelectItem>
<SelectItem value="gpt-5.4-mini">GPT-5.4 Mini (Faster)</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose which AI model to use for generating personas
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Number of Personas to Generate */}
<FormField
control={form.control}
name="personaCount"
render={({ field }) => (
<FormItem>
<FormLabel>Number of Personas to Generate</FormLabel>
<FormLabel>Number of personas</FormLabel>
<FormControl>
<Input type="number" min="1" max="20" {...field} />
</FormControl>
<FormDescription>
How many synthetic users do you need for your research?
</FormDescription>
<FormMessage />
</FormItem>
)}