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:
parent
d679691cc3
commit
c8d4591117
2 changed files with 109 additions and 208 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue