fix(i18n): translate Personas page — all hardcoded EN strings replaced with t() keys
Adds 74 keys to synthetic_users section in ru/uk/en locales. Covers: page title/subtitle, buttons, tabs (AI Recruiter / Manual Creation), AIRecruiterForm fields, UserCreator form labels, FolderTree labels, InputStrengthIndicator hints, filter/delete/move dialogs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
faf60b4b71
commit
e156ef897e
9 changed files with 424 additions and 147 deletions
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from "zod";
|
||||
import { Users } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
|
@ -19,6 +20,7 @@ interface AIRecruiterProps {
|
|||
}
|
||||
|
||||
export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecruiterProps) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { loadPersonas, savePersonas } = usePersonaStorage();
|
||||
|
|
@ -277,7 +279,7 @@ export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecr
|
|||
<div className="glass-panel rounded-xl p-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<h2 className="font-sf text-xl font-semibold">AI Persona Recruiter</h2>
|
||||
<h2 className="font-sf text-xl font-semibold">{t('synthetic_users.ai_recruiter_title')}</h2>
|
||||
</div>
|
||||
|
||||
{/* Progress Modal for Persona Generation */}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
|
|
@ -65,6 +66,7 @@ const FolderTree = ({
|
|||
const [folderToRename, setFolderToRename] = useState<Folder | null>(null);
|
||||
const [renameFolderName, setRenameFolderName] = useState('');
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [dragConfirmOpen, setDragConfirmOpen] = useState(false);
|
||||
|
|
@ -253,7 +255,7 @@ const FolderTree = ({
|
|||
<>
|
||||
<div className="w-full md:w-64 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Folders</h3>
|
||||
<h3 className="text-sm font-medium">{t('synthetic_users.label_folders')}</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -276,7 +278,7 @@ const FolderTree = ({
|
|||
<FolderTreeItem
|
||||
folder={{
|
||||
_id: 'all-personas-root',
|
||||
name: 'All Personas',
|
||||
name: t('synthetic_users.all_personas'),
|
||||
level: 0, // Same level as root folders for proper spacing
|
||||
parent_folder_id: null,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -559,7 +559,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
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>
|
||||
<h2 className="text-2xl font-sf font-semibold">{t('synthetic_users.title_create_synthetic_users')}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users size={15} className="text-muted-foreground" />
|
||||
<Input
|
||||
|
|
@ -570,7 +570,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
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>
|
||||
<span className="text-xs text-muted-foreground">{t('synthetic_users.label_users')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -596,7 +596,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Jane Smith" {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -611,11 +611,11 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="age"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Age Range</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_age_range')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select age range" />
|
||||
<SelectValue placeholder={t('synthetic_users.select_age_range')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
|
@ -637,11 +637,11 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="gender"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Gender</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_gender')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select gender" />
|
||||
<SelectValue placeholder={t('synthetic_users.select_gender')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
|
@ -662,7 +662,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="occupation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Occupation</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_occupation')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Software Engineer" {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -676,11 +676,11 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="education"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Education</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_education')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select education level" />
|
||||
<SelectValue placeholder={t('synthetic_users.select_education')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
|
@ -702,7 +702,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="location"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Location</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_location')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="New York, USA" {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -716,11 +716,11 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="ethnicity"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Ethnicity (Optional)</FormLabel>
|
||||
<FormLabel>{t('persona_editor.label_ethnicity')} (Optional)</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ethnicity" />
|
||||
<SelectValue placeholder={t('synthetic_users.select_ethnicity')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
|
|
@ -747,7 +747,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="personality"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Personality Traits</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_personality_traits')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Curious, analytical, detail-oriented" {...field} rows={3} />
|
||||
</FormControl>
|
||||
|
|
@ -764,7 +764,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="interests"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Interests</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_interests')}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Technology, fitness, cooking, travel" {...field} rows={3} />
|
||||
</FormControl>
|
||||
|
|
@ -777,14 +777,14 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">Behavioral Attributes</h3>
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">{t('synthetic_users.section_behavioral_attrs')}</h3>
|
||||
|
||||
{(
|
||||
[
|
||||
{ name: 'techSavviness', label: 'Tech Savviness' },
|
||||
{ name: 'brandLoyalty', label: 'Brand Loyalty' },
|
||||
{ name: 'priceConsciousness', label: 'Price Consciousness' },
|
||||
{ name: 'environmentalConcern', label: 'Environmental Concern' },
|
||||
{ name: 'techSavviness', label: t('persona_editor.label_tech_savviness') },
|
||||
{ name: 'brandLoyalty', label: t('persona_editor.label_brand_loyalty') },
|
||||
{ name: 'priceConsciousness', label: t('synthetic_users.label_price_consciousness') },
|
||||
{ name: 'environmentalConcern', label: t('persona_editor.label_environmental_concern') },
|
||||
] as const
|
||||
).map(({ name, label }) => (
|
||||
<FormField
|
||||
|
|
@ -821,7 +821,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="hasPurchasingPower"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<FormLabel>Purchasing Power</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_purchasing_power')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
|
|
@ -838,7 +838,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
name="hasChildren"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between">
|
||||
<FormLabel>Has Children</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_has_children')}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
|
|
@ -861,7 +861,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="font-medium text-lg mb-3">Goals</h3>
|
||||
<h3 className="font-medium text-lg mb-3">{t('persona_editor.section_goals')}</h3>
|
||||
{(form.watch('goals') || []).map((goal, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mb-2">
|
||||
<Input
|
||||
|
|
@ -887,12 +887,12 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
className="mt-2"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Goal
|
||||
{t('persona_editor.add_goal')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 pt-4 border-t">
|
||||
<h3 className="font-medium text-lg mb-3">Frustrations</h3>
|
||||
<h3 className="font-medium text-lg mb-3">{t('persona_editor.section_frustrations')}</h3>
|
||||
{(form.watch('frustrations') || []).map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mb-2">
|
||||
<Input
|
||||
|
|
@ -918,12 +918,12 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
className="mt-2"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Frustration
|
||||
{t('persona_editor.add_frustration')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 pt-4 border-t">
|
||||
<h3 className="font-medium text-lg mb-3">Motivations</h3>
|
||||
<h3 className="font-medium text-lg mb-3">{t('persona_editor.section_motivations')}</h3>
|
||||
{(form.watch('motivations') || []).map((item, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mb-2">
|
||||
<Input
|
||||
|
|
@ -949,7 +949,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
className="mt-2"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Motivation
|
||||
{t('persona_editor.add_motivation')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
@ -1105,7 +1105,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
className="mt-2"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Scenario
|
||||
{t('persona_editor.add_scenario')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1116,16 +1116,16 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
<TabsContent value="personality" className="mt-6">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="font-medium text-lg mb-4">OCEAN Personality Traits</h3>
|
||||
<h3 className="font-medium text-lg mb-4">{t('persona_editor.section_ocean')}</h3>
|
||||
|
||||
<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' },
|
||||
{ key: 'openness', label: t('persona_editor.label_openness'), desc: t('persona_editor.hint_openness') },
|
||||
{ key: 'conscientiousness', label: t('persona_editor.label_conscientiousness'), desc: t('persona_editor.hint_conscientiousness') },
|
||||
{ key: 'extraversion', label: t('persona_editor.label_extraversion'), desc: t('persona_editor.hint_extraversion') },
|
||||
{ key: 'agreeableness', label: t('persona_editor.label_agreeableness'), desc: t('persona_editor.hint_agreeableness') },
|
||||
{ key: 'neuroticism', label: t('persona_editor.label_neuroticism'), desc: t('persona_editor.hint_neuroticism') },
|
||||
] as const
|
||||
).map(({ key, label, desc }) => {
|
||||
const ocean = form.watch('oceanTraits') || { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50 };
|
||||
|
|
@ -1736,7 +1736,7 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="outline" type="button" onClick={() => form.reset()}>
|
||||
Reset
|
||||
{t('synthetic_users.btn_reset')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
|
|
@ -1744,7 +1744,11 @@ export default function UserCreator({ targetFolderId, targetFolderName }: UserCr
|
|||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isSubmitting ? 'Creating...' : `Create ${userCount > 1 ? `${userCount} Users` : 'User'}`}
|
||||
{isSubmitting
|
||||
? t('synthetic_users.btn_creating')
|
||||
: userCount > 1
|
||||
? t('synthetic_users.btn_create_user_many', { count: userCount })
|
||||
: t('synthetic_users.btn_create_user_one')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
|
@ -63,6 +64,7 @@ interface AIRecruiterFormProps {
|
|||
}
|
||||
|
||||
export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterFormProps) {
|
||||
const { t } = useTranslation();
|
||||
// State for enhance brief feature
|
||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||
const [showAssumptionsModal, setShowAssumptionsModal] = useState(false);
|
||||
|
|
@ -172,7 +174,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-1.5">
|
||||
Audience Brief
|
||||
{t('synthetic_users.label_audience_brief')}
|
||||
<FieldTooltip
|
||||
content="This defines the personas you'll be generating. Imagine you were briefing a recruitment agency to find these people in real life – what would you ask for?"
|
||||
example="We're looking for UK-based millennials aged 28-35 who are considering switching their streaming service provider. They should be tech-savvy, price-conscious, and currently subscribe to at least two streaming platforms. We want a mix of urban and suburban residents, with varying household sizes."
|
||||
|
|
@ -201,7 +203,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex items-center gap-1.5">
|
||||
Research Objective
|
||||
{t('synthetic_users.label_research_objective')}
|
||||
<FieldTooltip
|
||||
content="This helps create personas with backgrounds related to your research area. We use the information here to create 'life scenarios' that connect your personas to certain topics. Think of it like letting your participants know what kinds of questions you'll be asking them ahead of time. You will have the chance to write a more detailed research brief later."
|
||||
example="Understanding switching behaviours in TV streaming markets. We want to explore what triggers consideration of new platforms, how users evaluate alternatives, and what factors drive final switching decisions. We're particularly interested in the role of exclusive content and pricing."
|
||||
|
|
@ -237,12 +239,12 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
{isEnhancing ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 animate-spin" />
|
||||
Enhancing...
|
||||
{t('synthetic_users.btn_enhancing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lightbulb className="h-4 w-4" />
|
||||
Enhance Brief
|
||||
{t('synthetic_users.btn_enhance_brief')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
|
@ -254,7 +256,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
<AccordionItem value="customer-data-upload" className="border-none">
|
||||
<AccordionTrigger className="text-lg font-semibold hover:no-underline py-0">
|
||||
<span className="flex items-center gap-1.5">
|
||||
Customer Data Upload (Optional)
|
||||
{t('synthetic_users.label_customer_data_upload')}
|
||||
<FieldTooltip
|
||||
content="Additional information about your target audience improves the accuracy of the personas generated. You can upload anything from segmentations to transcripts or even existing pen portraits"
|
||||
/>
|
||||
|
|
@ -346,16 +348,16 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
name="llm_model"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Model</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_model')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select model" />
|
||||
<SelectValue placeholder={t('synthetic_users.select_model_placeholder')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-5.4">GPT-5.4 (Recommended)</SelectItem>
|
||||
<SelectItem value="gpt-5.4-mini">GPT-5.4 Mini (Faster)</SelectItem>
|
||||
<SelectItem value="gpt-5.4">{t('focus_group_setup.model_gpt54')}</SelectItem>
|
||||
<SelectItem value="gpt-5.4-mini">{t('focus_group_setup.model_gpt54_mini')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
|
|
@ -369,7 +371,7 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
name="personaCount"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Number of personas</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_persona_count')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" min="1" max="20" {...field} />
|
||||
</FormControl>
|
||||
|
|
@ -382,14 +384,14 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
{/* Advanced Controls - Collapsible */}
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advanced-controls">
|
||||
<AccordionTrigger>Advanced Controls</AccordionTrigger>
|
||||
<AccordionTrigger>{t('synthetic_users.label_advanced_controls')}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="temperature"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Creativity Level</FormLabel>
|
||||
<FormLabel>{t('synthetic_users.label_creativity_level')}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
|
|
@ -402,8 +404,8 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>More Focused & Consistent</span>
|
||||
<span>More Diverse & Creative</span>
|
||||
<span>{t('synthetic_users.creativity_focused')}</span>
|
||||
<span>{t('synthetic_users.creativity_diverse')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
|
@ -435,18 +437,18 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
{isGenerating ? (
|
||||
<>
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
AI Generating...
|
||||
{t('synthetic_users.btn_ai_generating')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Generate Personas
|
||||
{t('synthetic_users.btn_generate_personas')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{isGenerating && (
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
Generating personas. This may take 1-2 minutes...
|
||||
{t('synthetic_users.generating_info')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -458,10 +460,10 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-primary" />
|
||||
Brief Enhanced Successfully
|
||||
{t('synthetic_users.modal_brief_enhanced_title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your inputs have been enhanced with the following additions:
|
||||
{t('synthetic_users.modal_brief_enhanced_desc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
|
|
@ -476,13 +478,13 @@ export default function AIRecruiterForm({ onSubmit, isGenerating }: AIRecruiterF
|
|||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No specific assumptions were made during enhancement.
|
||||
{t('synthetic_users.modal_brief_no_assumptions')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setShowAssumptionsModal(false)}>
|
||||
Got it
|
||||
{t('synthetic_users.btn_got_it')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InputStrengthIndicatorProps {
|
||||
|
|
@ -12,13 +13,14 @@ export function InputStrengthIndicator({
|
|||
minWords,
|
||||
className
|
||||
}: InputStrengthIndicatorProps) {
|
||||
const { t } = useTranslation();
|
||||
const wordCount = text.trim() ? text.trim().split(/\s+/).filter(Boolean).length : 0;
|
||||
const percentage = (wordCount / minWords) * 100;
|
||||
|
||||
const getState = () => {
|
||||
if (percentage >= 100) return { level: 3, label: "Good length", color: "bg-green-500" };
|
||||
if (percentage >= 33) return { level: 2, label: "Getting there", color: "bg-yellow-500" };
|
||||
return { level: 1, label: "Add more detail", color: "bg-red-500" };
|
||||
if (percentage >= 100) return { level: 3, label: t('synthetic_users.indicator_good_length'), color: "bg-green-500" };
|
||||
if (percentage >= 33) return { level: 2, label: t('synthetic_users.indicator_getting_there'), color: "bg-yellow-500" };
|
||||
return { level: 1, label: t('synthetic_users.indicator_add_more'), color: "bg-red-500" };
|
||||
};
|
||||
|
||||
const state = getState();
|
||||
|
|
@ -36,7 +38,7 @@ export function InputStrengthIndicator({
|
|||
/>
|
||||
))}
|
||||
</div>
|
||||
<span>{wordCount} / {minWords} words</span>
|
||||
<span>{wordCount} / {minWords} {t('synthetic_users.indicator_words')}</span>
|
||||
<span className="text-muted-foreground/70">·</span>
|
||||
<span>{state.label}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -337,7 +337,95 @@
|
|||
"toast_delete_error_many": "Failed to delete {{count}} personas",
|
||||
"toast_no_download": "No personas to download",
|
||||
"toast_generating_summaries": "Generating persona summaries...",
|
||||
"search_placeholder": "Search personas…"
|
||||
"search_placeholder": "Search personas…",
|
||||
"page_title": "Synthetic Personas",
|
||||
"page_subtitle": "Create and manage AI-generated research participants",
|
||||
"btn_create_new": "Create New Personas",
|
||||
"btn_view_all": "View All Personas",
|
||||
"btn_download_summary": "Download Persona Summary",
|
||||
"btn_generating_summary": "Generating Summary...",
|
||||
"modal_generating_summaries_title": "Generating Persona Summaries",
|
||||
"library_title": "Your Synthetic Persona Library",
|
||||
"no_personas_found": "No personas found matching your criteria.",
|
||||
"select_all": "Select All",
|
||||
"actions_count": "Actions ({{count}})",
|
||||
"btn_delete": "Delete",
|
||||
"btn_cancel": "Cancel",
|
||||
"btn_move": "Move",
|
||||
"btn_filter": "Filter",
|
||||
"btn_reset_filters": "Reset",
|
||||
"btn_apply_filters": "Apply Filters",
|
||||
"btn_generate_summary": "Generate Summary",
|
||||
"label_folders": "Folders",
|
||||
"all_personas": "All Personas",
|
||||
"all_personas_remove": "All Personas (Remove from folders)",
|
||||
"dialog_delete_personas_title": "Delete Personas",
|
||||
"dialog_delete_personas_desc_one": "Are you sure you want to delete {{count}} persona? This action cannot be undone.",
|
||||
"dialog_delete_personas_desc_many": "Are you sure you want to delete {{count}} personas? This action cannot be undone.",
|
||||
"dialog_delete_folder_title": "Delete Folder",
|
||||
"dialog_delete_folder_desc": "Are you sure you want to delete the folder \"{{name}}\"?",
|
||||
"dialog_delete_folder_note": "Note: Any personas in this folder will not be deleted — they will still be available under All Personas.",
|
||||
"dialog_move_title": "Move to Folder",
|
||||
"dialog_move_desc_one": "Choose folders to add {{count}} selected persona to. Personas can belong to multiple folders.",
|
||||
"dialog_move_desc_many": "Choose folders to add {{count}} selected personas to. Personas can belong to multiple folders.",
|
||||
"dialog_filter_title": "Filter Personas",
|
||||
"dialog_filter_desc": "Select attributes to filter personas by. Multiple selections within a category use OR logic, different categories use AND logic.",
|
||||
"filter_active_count": "{{count}} active filters",
|
||||
"filter_folder_assignment": "Folder Assignment",
|
||||
"filter_has_folder": "Has folder assignment",
|
||||
"filter_no_folder": "No folder assignment",
|
||||
"filter_no_matches": "(no matches)",
|
||||
"dialog_select_model_title": "Select AI Model for Summary Generation",
|
||||
"dialog_select_model_desc": "Choose which AI model to use for generating persona summaries",
|
||||
"tab_ai_recruiter": "AI Recruiter",
|
||||
"tab_manual_creation": "Manual Creation",
|
||||
"label_users": "users",
|
||||
"title_create_synthetic_users": "Create Synthetic Users",
|
||||
"label_age_range": "Age Range",
|
||||
"select_age_range": "Select age range",
|
||||
"select_gender": "Select gender",
|
||||
"select_education": "Select education level",
|
||||
"select_ethnicity": "Select ethnicity",
|
||||
"label_personality_traits": "Personality Traits",
|
||||
"label_interests": "Interests",
|
||||
"section_behavioral_attrs": "Behavioral Attributes",
|
||||
"label_price_consciousness": "Price Consciousness",
|
||||
"label_purchasing_power": "Purchasing Power",
|
||||
"label_has_children": "Has Children",
|
||||
"btn_reset": "Reset",
|
||||
"btn_create_user_one": "Create User",
|
||||
"btn_create_user_many": "Create {{count}} Users",
|
||||
"btn_creating": "Creating...",
|
||||
"ai_recruiter_title": "AI Persona Recruiter",
|
||||
"label_audience_brief": "Audience Brief",
|
||||
"label_research_objective": "Research Objective",
|
||||
"btn_enhance_brief": "Enhance Brief",
|
||||
"btn_enhancing": "Enhancing...",
|
||||
"label_customer_data_upload": "Customer Data Upload (Optional)",
|
||||
"label_model": "Model",
|
||||
"select_model_placeholder": "Select model",
|
||||
"label_persona_count": "Number of personas",
|
||||
"label_advanced_controls": "Advanced Controls",
|
||||
"label_creativity_level": "Creativity Level",
|
||||
"creativity_focused": "More Focused & Consistent",
|
||||
"creativity_diverse": "More Diverse & Creative",
|
||||
"btn_generate_personas": "Generate Personas",
|
||||
"btn_ai_generating": "AI Generating...",
|
||||
"generating_info": "Generating personas. This may take 1-2 minutes...",
|
||||
"modal_brief_enhanced_title": "Brief Enhanced Successfully",
|
||||
"modal_brief_enhanced_desc": "Your inputs have been enhanced with the following additions:",
|
||||
"modal_brief_no_assumptions": "No specific assumptions were made during enhancement.",
|
||||
"btn_got_it": "Got it",
|
||||
"indicator_words": "words",
|
||||
"indicator_add_more": "Add more detail",
|
||||
"indicator_getting_there": "Getting there",
|
||||
"indicator_good_length": "Good length",
|
||||
"dropdown_create_fg": "Create Focus Group with selected Personas",
|
||||
"dropdown_move_to_folder": "Move to folder",
|
||||
"dropdown_remove_from_folder": "Remove from {{folder}}",
|
||||
"dropdown_download_markdown": "Download Full Persona Profiles (Markdown)",
|
||||
"dropdown_download_json": "Download Full Persona Profiles (JSON)",
|
||||
"dropdown_download_csv": "Download Full Persona Profiles (CSV)"
|
||||
},
|
||||
"focus_group_session": {
|
||||
"toast_ws_restored": "Real-time updates restored",
|
||||
|
|
|
|||
|
|
@ -337,7 +337,95 @@
|
|||
"toast_delete_error_many": "Не удалось удалить {{count}} персон",
|
||||
"toast_no_download": "Нет персон для загрузки",
|
||||
"toast_generating_summaries": "Генерация сводок персон...",
|
||||
"search_placeholder": "Поиск персон…"
|
||||
"search_placeholder": "Поиск персон…",
|
||||
"page_title": "Синтетические персоны",
|
||||
"page_subtitle": "Создавайте AI-персоны для исследований",
|
||||
"btn_create_new": "Создать новые персоны",
|
||||
"btn_view_all": "Смотреть все персоны",
|
||||
"btn_download_summary": "Скачать сводку персон",
|
||||
"btn_generating_summary": "Генерация сводки...",
|
||||
"modal_generating_summaries_title": "Генерация сводок персон",
|
||||
"library_title": "Библиотека синтетических персон",
|
||||
"no_personas_found": "Персон по вашему запросу не найдено.",
|
||||
"select_all": "Выбрать все",
|
||||
"actions_count": "Действия ({{count}})",
|
||||
"btn_delete": "Удалить",
|
||||
"btn_cancel": "Отмена",
|
||||
"btn_move": "Переместить",
|
||||
"btn_filter": "Фильтр",
|
||||
"btn_reset_filters": "Сбросить",
|
||||
"btn_apply_filters": "Применить фильтры",
|
||||
"btn_generate_summary": "Сгенерировать сводку",
|
||||
"label_folders": "Папки",
|
||||
"all_personas": "Все персоны",
|
||||
"all_personas_remove": "Все персоны (удалить из папок)",
|
||||
"dialog_delete_personas_title": "Удалить персоны",
|
||||
"dialog_delete_personas_desc_one": "Вы уверены, что хотите удалить {{count}} персону? Это действие нельзя отменить.",
|
||||
"dialog_delete_personas_desc_many": "Вы уверены, что хотите удалить {{count}} персон? Это действие нельзя отменить.",
|
||||
"dialog_delete_folder_title": "Удалить папку",
|
||||
"dialog_delete_folder_desc": "Вы уверены, что хотите удалить папку «{{name}}»?",
|
||||
"dialog_delete_folder_note": "Примечание: персоны в этой папке не будут удалены — они по-прежнему будут доступны в разделе «Все персоны».",
|
||||
"dialog_move_title": "Переместить в папку",
|
||||
"dialog_move_desc_one": "Выберите папки для добавления {{count}} персоны. Персоны могут находиться в нескольких папках.",
|
||||
"dialog_move_desc_many": "Выберите папки для добавления {{count}} персон. Персоны могут находиться в нескольких папках.",
|
||||
"dialog_filter_title": "Фильтр персон",
|
||||
"dialog_filter_desc": "Выберите атрибуты для фильтрации персон. Несколько значений в одной категории работают по OR, разные категории — AND.",
|
||||
"filter_active_count": "{{count}} активных фильтров",
|
||||
"filter_folder_assignment": "Назначение папки",
|
||||
"filter_has_folder": "Есть назначение папки",
|
||||
"filter_no_folder": "Нет назначения папки",
|
||||
"filter_no_matches": "(нет совпадений)",
|
||||
"dialog_select_model_title": "Выберите AI-модель для генерации",
|
||||
"dialog_select_model_desc": "Выберите AI-модель для генерации сводок персон",
|
||||
"tab_ai_recruiter": "AI-рекрутер",
|
||||
"tab_manual_creation": "Ручное создание",
|
||||
"label_users": "пользователей",
|
||||
"title_create_synthetic_users": "Создать синтетических пользователей",
|
||||
"label_age_range": "Возрастная группа",
|
||||
"select_age_range": "Выберите возраст",
|
||||
"select_gender": "Выберите пол",
|
||||
"select_education": "Выберите уровень образования",
|
||||
"select_ethnicity": "Выберите этническость",
|
||||
"label_personality_traits": "Черты личности",
|
||||
"label_interests": "Интересы",
|
||||
"section_behavioral_attrs": "Поведенческие атрибуты",
|
||||
"label_price_consciousness": "Ценовая осознанность",
|
||||
"label_purchasing_power": "Покупательная способность",
|
||||
"label_has_children": "Есть дети",
|
||||
"btn_reset": "Сбросить",
|
||||
"btn_create_user_one": "Создать пользователя",
|
||||
"btn_create_user_many": "Создать {{count}} пользователей",
|
||||
"btn_creating": "Создание...",
|
||||
"ai_recruiter_title": "AI-рекрутер персон",
|
||||
"label_audience_brief": "Бриф аудитории",
|
||||
"label_research_objective": "Цель исследования",
|
||||
"btn_enhance_brief": "Улучшить бриф",
|
||||
"btn_enhancing": "Улучшение...",
|
||||
"label_customer_data_upload": "Загрузка данных клиентов (необязательно)",
|
||||
"label_model": "Модель",
|
||||
"select_model_placeholder": "Выберите модель",
|
||||
"label_persona_count": "Количество персон",
|
||||
"label_advanced_controls": "Расширенные настройки",
|
||||
"label_creativity_level": "Уровень креативности",
|
||||
"creativity_focused": "Более сфокусированный",
|
||||
"creativity_diverse": "Более разнообразный",
|
||||
"btn_generate_personas": "Сгенерировать персоны",
|
||||
"btn_ai_generating": "AI генерирует...",
|
||||
"generating_info": "Генерация персон. Это может занять 1–2 минуты…",
|
||||
"modal_brief_enhanced_title": "Бриф успешно улучшен",
|
||||
"modal_brief_enhanced_desc": "Ваши данные были дополнены следующими сведениями:",
|
||||
"modal_brief_no_assumptions": "В процессе улучшения не было сделано конкретных допущений.",
|
||||
"btn_got_it": "Понятно",
|
||||
"indicator_words": "слов",
|
||||
"indicator_add_more": "Добавьте деталей",
|
||||
"indicator_getting_there": "Почти достаточно",
|
||||
"indicator_good_length": "Хороший объём",
|
||||
"dropdown_create_fg": "Создать фокус-группу с выбранными",
|
||||
"dropdown_move_to_folder": "Переместить в папку",
|
||||
"dropdown_remove_from_folder": "Удалить из {{folder}}",
|
||||
"dropdown_download_markdown": "Скачать профили (Markdown)",
|
||||
"dropdown_download_json": "Скачать профили (JSON)",
|
||||
"dropdown_download_csv": "Скачать профили (CSV)"
|
||||
},
|
||||
"focus_group_session": {
|
||||
"toast_ws_restored": "Обновления в реальном времени восстановлены",
|
||||
|
|
|
|||
|
|
@ -337,7 +337,95 @@
|
|||
"toast_delete_error_many": "Не вдалося видалити {{count}} персон",
|
||||
"toast_no_download": "Немає персон для завантаження",
|
||||
"toast_generating_summaries": "Генерація зведень персон...",
|
||||
"search_placeholder": "Пошук персон…"
|
||||
"search_placeholder": "Пошук персон…",
|
||||
"page_title": "Синтетичні персони",
|
||||
"page_subtitle": "Створюйте AI-персони для досліджень",
|
||||
"btn_create_new": "Створити нові персони",
|
||||
"btn_view_all": "Переглянути всі персони",
|
||||
"btn_download_summary": "Завантажити зведення персон",
|
||||
"btn_generating_summary": "Генерація зведення...",
|
||||
"modal_generating_summaries_title": "Генерація зведень персон",
|
||||
"library_title": "Бібліотека синтетичних персон",
|
||||
"no_personas_found": "Персон за вашим запитом не знайдено.",
|
||||
"select_all": "Вибрати всі",
|
||||
"actions_count": "Дії ({{count}})",
|
||||
"btn_delete": "Видалити",
|
||||
"btn_cancel": "Скасувати",
|
||||
"btn_move": "Перемістити",
|
||||
"btn_filter": "Фільтр",
|
||||
"btn_reset_filters": "Скинути",
|
||||
"btn_apply_filters": "Застосувати фільтри",
|
||||
"btn_generate_summary": "Сформувати зведення",
|
||||
"label_folders": "Папки",
|
||||
"all_personas": "Усі персони",
|
||||
"all_personas_remove": "Усі персони (видалити з папок)",
|
||||
"dialog_delete_personas_title": "Видалити персони",
|
||||
"dialog_delete_personas_desc_one": "Ви впевнені, що хочете видалити {{count}} персону? Цю дію не можна скасувати.",
|
||||
"dialog_delete_personas_desc_many": "Ви впевнені, що хочете видалити {{count}} персон? Цю дію не можна скасувати.",
|
||||
"dialog_delete_folder_title": "Видалити папку",
|
||||
"dialog_delete_folder_desc": "Ви впевнені, що хочете видалити папку «{{name}}»?",
|
||||
"dialog_delete_folder_note": "Примітка: персони з цієї папки не будуть видалені — вони залишаться доступними в розділі «Усі персони».",
|
||||
"dialog_move_title": "Перемістити до папки",
|
||||
"dialog_move_desc_one": "Виберіть папки для додавання {{count}} персони. Персони можуть знаходитися в кількох папках.",
|
||||
"dialog_move_desc_many": "Виберіть папки для додавання {{count}} персон. Персони можуть знаходитися в кількох папках.",
|
||||
"dialog_filter_title": "Фільтр персон",
|
||||
"dialog_filter_desc": "Виберіть атрибути для фільтрації персон. Кілька значень в одній категорії — OR, різні категорії — AND.",
|
||||
"filter_active_count": "{{count}} активних фільтрів",
|
||||
"filter_folder_assignment": "Призначення папки",
|
||||
"filter_has_folder": "Є призначення папки",
|
||||
"filter_no_folder": "Немає призначення папки",
|
||||
"filter_no_matches": "(немає збігів)",
|
||||
"dialog_select_model_title": "Виберіть AI-модель для генерації",
|
||||
"dialog_select_model_desc": "Виберіть AI-модель для генерації зведень персон",
|
||||
"tab_ai_recruiter": "AI-рекрутер",
|
||||
"tab_manual_creation": "Ручне створення",
|
||||
"label_users": "користувачів",
|
||||
"title_create_synthetic_users": "Створити синтетичних користувачів",
|
||||
"label_age_range": "Вікова група",
|
||||
"select_age_range": "Виберіть вік",
|
||||
"select_gender": "Виберіть стать",
|
||||
"select_education": "Виберіть рівень освіти",
|
||||
"select_ethnicity": "Виберіть етнічність",
|
||||
"label_personality_traits": "Риси особистості",
|
||||
"label_interests": "Інтереси",
|
||||
"section_behavioral_attrs": "Поведінкові атрибути",
|
||||
"label_price_consciousness": "Цінова свідомість",
|
||||
"label_purchasing_power": "Купівельна спроможність",
|
||||
"label_has_children": "Є діти",
|
||||
"btn_reset": "Скинути",
|
||||
"btn_create_user_one": "Створити користувача",
|
||||
"btn_create_user_many": "Створити {{count}} користувачів",
|
||||
"btn_creating": "Створення...",
|
||||
"ai_recruiter_title": "AI-рекрутер персон",
|
||||
"label_audience_brief": "Бриф аудиторії",
|
||||
"label_research_objective": "Мета дослідження",
|
||||
"btn_enhance_brief": "Покращити бриф",
|
||||
"btn_enhancing": "Покращення...",
|
||||
"label_customer_data_upload": "Завантаження даних клієнтів (необов'язково)",
|
||||
"label_model": "Модель",
|
||||
"select_model_placeholder": "Виберіть модель",
|
||||
"label_persona_count": "Кількість персон",
|
||||
"label_advanced_controls": "Розширені налаштування",
|
||||
"label_creativity_level": "Рівень креативності",
|
||||
"creativity_focused": "Більш сфокусований",
|
||||
"creativity_diverse": "Більш різноманітний",
|
||||
"btn_generate_personas": "Згенерувати персони",
|
||||
"btn_ai_generating": "AI генерує...",
|
||||
"generating_info": "Генерація персон. Це може зайняти 1–2 хвилини…",
|
||||
"modal_brief_enhanced_title": "Бриф успішно покращено",
|
||||
"modal_brief_enhanced_desc": "Ваші дані були доповнені такою інформацією:",
|
||||
"modal_brief_no_assumptions": "У процесі покращення не було зроблено конкретних припущень.",
|
||||
"btn_got_it": "Зрозуміло",
|
||||
"indicator_words": "слів",
|
||||
"indicator_add_more": "Додайте деталей",
|
||||
"indicator_getting_there": "Майже достатньо",
|
||||
"indicator_good_length": "Гарний обсяг",
|
||||
"dropdown_create_fg": "Створити фокус-групу з вибраними",
|
||||
"dropdown_move_to_folder": "Перемістити до папки",
|
||||
"dropdown_remove_from_folder": "Видалити з {{folder}}",
|
||||
"dropdown_download_markdown": "Завантажити профілі (Markdown)",
|
||||
"dropdown_download_json": "Завантажити профілі (JSON)",
|
||||
"dropdown_download_csv": "Завантажити профілі (CSV)"
|
||||
},
|
||||
"focus_group_session": {
|
||||
"toast_ws_restored": "Оновлення в реальному часі відновлено",
|
||||
|
|
|
|||
|
|
@ -1158,8 +1158,8 @@ const SyntheticUsers = () => {
|
|||
<main className="pt-20 pb-16 px-4 sm:px-6 lg:px-8 max-w-7xl mx-auto">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8">
|
||||
<div>
|
||||
<h1 className="font-sf text-3xl font-bold text-foreground">Synthetic Personas</h1>
|
||||
<p className="text-muted-foreground mt-1">Create and manage AI-generated research participants</p>
|
||||
<h1 className="font-sf text-3xl font-bold text-foreground">{t('synthetic_users.page_title')}</h1>
|
||||
<p className="text-muted-foreground mt-1">{t('synthetic_users.page_subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 sm:mt-0 flex flex-col items-end gap-3">
|
||||
|
|
@ -1172,14 +1172,14 @@ const SyntheticUsers = () => {
|
|||
className="flex items-center gap-2 hover-transition"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{summaryGenerationState.isGenerating ? 'Generating Summary...' : 'Download Persona Summary'}
|
||||
{summaryGenerationState.isGenerating ? t('synthetic_users.btn_generating_summary') : t('synthetic_users.btn_download_summary')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setMode(mode === 'view' ? 'create' : 'view')}
|
||||
className="hover-transition"
|
||||
>
|
||||
{mode === 'view' ? 'Create New Personas' : 'View All Personas'}
|
||||
{mode === 'view' ? t('synthetic_users.btn_create_new') : t('synthetic_users.btn_view_all')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -1195,7 +1195,7 @@ const SyntheticUsers = () => {
|
|||
hasError={summaryGenerationState.hasError}
|
||||
isCancelling={summaryGenerationState.isCancelling}
|
||||
taskId={summaryGenerationState.taskId}
|
||||
title="Generating Persona Summaries"
|
||||
title={t('synthetic_users.modal_generating_summaries_title')}
|
||||
description={summaryProgressDescription}
|
||||
onCancel={summaryGenerationControls.cancelGeneration}
|
||||
onComplete={handleSummaryProgressComplete}
|
||||
|
|
@ -1241,7 +1241,7 @@ const SyntheticUsers = () => {
|
|||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span>Actions ({selectedPersonas.size})</span>
|
||||
<span>{t('synthetic_users.actions_count', { count: selectedPersonas.size })}</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
|
@ -1267,7 +1267,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
Create Focus Group with selected Personas
|
||||
{t('synthetic_users.dropdown_create_fg')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
|
|
@ -1289,7 +1289,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
Move to folder
|
||||
{t('synthetic_users.dropdown_move_to_folder')}
|
||||
</DropdownMenuItem>
|
||||
{selectedFolder !== DEFAULT_FOLDER_ID && (
|
||||
<DropdownMenuItem
|
||||
|
|
@ -1301,7 +1301,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
Remove from {folders.find(f => f._id === selectedFolder)?.name || 'folder'}
|
||||
{t('synthetic_users.dropdown_remove_from_folder', { folder: folders.find(f => f._id === selectedFolder)?.name || '' })}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
|
|
@ -1313,7 +1313,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download Full Persona Profiles (Markdown)
|
||||
{t('synthetic_users.dropdown_download_markdown')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
|
|
@ -1324,7 +1324,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download Full Persona Profiles (JSON)
|
||||
{t('synthetic_users.dropdown_download_json')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
|
|
@ -1335,7 +1335,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download Full Persona Profiles (CSV)
|
||||
{t('synthetic_users.dropdown_download_csv')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -1347,7 +1347,7 @@ const SyntheticUsers = () => {
|
|||
onClick={() => setIsFilterOpen(true)}
|
||||
>
|
||||
<Filter className="h-4 w-4" />
|
||||
<span>Filter{Object.values(activeFilters).some(arr => arr.length > 0) ? ` (${Object.values(activeFilters).reduce((count, arr) => count + arr.length, 0)})` : ''}</span>
|
||||
<span>{t('synthetic_users.btn_filter')}{Object.values(activeFilters).some(arr => arr.length > 0) ? ` (${Object.values(activeFilters).reduce((count, arr) => count + arr.length, 0)})` : ''}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1357,9 +1357,9 @@ const SyntheticUsers = () => {
|
|||
<div className="flex items-center gap-2">
|
||||
<Users className="h-5 w-5 text-primary" />
|
||||
<h2 className="font-sf text-xl font-semibold">
|
||||
{selectedFolder === DEFAULT_FOLDER_ID
|
||||
? 'Your Synthetic Persona Library'
|
||||
: folders.find(f => f._id === selectedFolder)?.name || 'Personas'}
|
||||
{selectedFolder === DEFAULT_FOLDER_ID
|
||||
? t('synthetic_users.library_title')
|
||||
: folders.find(f => f._id === selectedFolder)?.name || t('synthetic_users.page_title')}
|
||||
</h2>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({filteredPersonas.length})
|
||||
|
|
@ -1375,7 +1375,7 @@ const SyntheticUsers = () => {
|
|||
className="mr-2"
|
||||
/>
|
||||
<label htmlFor="select-all" className="text-sm cursor-pointer">
|
||||
Select All
|
||||
{t('synthetic_users.select_all')}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1402,7 +1402,7 @@ const SyntheticUsers = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">No personas found matching your criteria.</p>
|
||||
<p className="text-muted-foreground">{t('synthetic_users.no_personas_found')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1426,10 +1426,11 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Personas</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('synthetic_users.dialog_delete_personas_title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete {selectedPersonas.size} selected persona{selectedPersonas.size !== 1 ? 's' : ''}?
|
||||
This action cannot be undone.
|
||||
{selectedPersonas.size === 1
|
||||
? t('synthetic_users.dialog_delete_personas_desc_one', { count: selectedPersonas.size })
|
||||
: t('synthetic_users.dialog_delete_personas_desc_many', { count: selectedPersonas.size })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
|
@ -1437,13 +1438,13 @@ const SyntheticUsers = () => {
|
|||
// Clear selection after canceling
|
||||
setTimeout(() => setSelectedPersonas(new Set()), 50);
|
||||
}}>
|
||||
Cancel
|
||||
{t('synthetic_users.btn_cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={deleteSelectedPersonas}
|
||||
<AlertDialogAction
|
||||
onClick={deleteSelectedPersonas}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
{t('synthetic_users.btn_delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
@ -1461,20 +1462,20 @@ const SyntheticUsers = () => {
|
|||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Folder</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t('synthetic_users.dialog_delete_folder_title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the folder "{folderToDelete?.name}"?
|
||||
{t('synthetic_users.dialog_delete_folder_desc', { name: folderToDelete?.name || '' })}
|
||||
<br /><br />
|
||||
<strong>Note:</strong> Any personas in this folder will not be deleted - they will still be available under 'All Personas' after folder deletion.
|
||||
{t('synthetic_users.dialog_delete_folder_note')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={completeDeleteFolder}
|
||||
<AlertDialogCancel>{t('synthetic_users.btn_cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={completeDeleteFolder}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
Delete
|
||||
{t('synthetic_users.btn_delete')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
|
@ -1496,9 +1497,11 @@ const SyntheticUsers = () => {
|
|||
className="z-50" // Ensure highest z-index
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move to Folder</DialogTitle>
|
||||
<DialogTitle>{t('synthetic_users.dialog_move_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose one or more folders to add {selectedPersonas.size} selected persona{selectedPersonas.size !== 1 ? 's' : ''} to. Personas can belong to multiple folders.
|
||||
{selectedPersonas.size === 1
|
||||
? t('synthetic_users.dialog_move_desc_one', { count: selectedPersonas.size })
|
||||
: t('synthetic_users.dialog_move_desc_many', { count: selectedPersonas.size })}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -1521,7 +1524,7 @@ const SyntheticUsers = () => {
|
|||
/>
|
||||
<Label htmlFor="folder-all" className="flex items-center gap-2 cursor-pointer">
|
||||
<Folder className="h-4 w-4" />
|
||||
<span>All Personas (Remove from folders)</span>
|
||||
<span>{t('synthetic_users.all_personas_remove')}</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
|
|
@ -1589,9 +1592,9 @@ const SyntheticUsers = () => {
|
|||
setTargetFolders(new Set());
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('synthetic_users.btn_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
onClick={async (e) => {
|
||||
// Prevent default behavior and stop propagation
|
||||
e.preventDefault();
|
||||
|
|
@ -1624,7 +1627,7 @@ const SyntheticUsers = () => {
|
|||
}}
|
||||
disabled={targetFolders.size === 0}
|
||||
>
|
||||
Move
|
||||
{t('synthetic_users.btn_move')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -1659,10 +1662,9 @@ const SyntheticUsers = () => {
|
|||
{/* Sticky Header */}
|
||||
<div className="sticky top-0 bg-background border-b shadow-sm pb-4 z-10">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Filter Personas</DialogTitle>
|
||||
<DialogTitle>{t('synthetic_users.dialog_filter_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select attributes to filter personas by. Multiple selections within a category use OR logic,
|
||||
different categories use AND logic. Filter options dynamically update to show only relevant values.
|
||||
{t('synthetic_users.dialog_filter_desc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
|
@ -1673,7 +1675,7 @@ const SyntheticUsers = () => {
|
|||
{Object.values(workingFilters).some(arr => arr.length > 0) && (
|
||||
<div className="bg-muted/30 p-3 rounded-md">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{Object.values(workingFilters).reduce((count, arr) => count + arr.length, 0)} active filters
|
||||
{t('synthetic_users.filter_active_count', { count: Object.values(workingFilters).reduce((count, arr) => count + arr.length, 0) })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1774,7 +1776,7 @@ const SyntheticUsers = () => {
|
|||
>
|
||||
{option}
|
||||
{isSelected && !isAvailable && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">(no matches)</span>
|
||||
<span className="ml-1 text-xs text-muted-foreground">{t('synthetic_users.filter_no_matches')}</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -1788,47 +1790,46 @@ const SyntheticUsers = () => {
|
|||
return (
|
||||
<>
|
||||
{renderFilterSection(
|
||||
'Gender',
|
||||
'gender',
|
||||
noActiveFilters ? filterOptions.gender : getFilteredOptions('gender').gender,
|
||||
t('persona_editor.label_gender'),
|
||||
'gender',
|
||||
noActiveFilters ? filterOptions.gender : getFilteredOptions('gender').gender,
|
||||
3
|
||||
)}
|
||||
{renderFilterSection(
|
||||
'Age',
|
||||
'age',
|
||||
noActiveFilters ? filterOptions.age : getFilteredOptions('age').age,
|
||||
t('persona_editor.label_age'),
|
||||
'age',
|
||||
noActiveFilters ? filterOptions.age : getFilteredOptions('age').age,
|
||||
3
|
||||
)}
|
||||
{renderFilterSection(
|
||||
'Ethnicity',
|
||||
'ethnicity',
|
||||
noActiveFilters ? filterOptions.ethnicity : getFilteredOptions('ethnicity').ethnicity,
|
||||
t('persona_editor.label_ethnicity'),
|
||||
'ethnicity',
|
||||
noActiveFilters ? filterOptions.ethnicity : getFilteredOptions('ethnicity').ethnicity,
|
||||
2
|
||||
)}
|
||||
{renderFilterSection(
|
||||
'Location',
|
||||
'location',
|
||||
noActiveFilters ? filterOptions.location : getFilteredOptions('location').location,
|
||||
t('persona_editor.label_location'),
|
||||
'location',
|
||||
noActiveFilters ? filterOptions.location : getFilteredOptions('location').location,
|
||||
2
|
||||
)}
|
||||
{renderFilterSection(
|
||||
'Occupation',
|
||||
'occupation',
|
||||
noActiveFilters ? filterOptions.occupation : getFilteredOptions('occupation').occupation,
|
||||
t('persona_editor.label_occupation'),
|
||||
'occupation',
|
||||
noActiveFilters ? filterOptions.occupation : getFilteredOptions('occupation').occupation,
|
||||
2
|
||||
)}
|
||||
|
||||
{/* Tech Savviness */}
|
||||
|
||||
{renderFilterSection(
|
||||
'Tech Savviness',
|
||||
'techSavviness',
|
||||
noActiveFilters ? filterOptions.techSavviness : getFilteredOptions('techSavviness').techSavviness,
|
||||
t('persona_editor.label_tech_savviness'),
|
||||
'techSavviness',
|
||||
noActiveFilters ? filterOptions.techSavviness : getFilteredOptions('techSavviness').techSavviness,
|
||||
3
|
||||
)}
|
||||
|
||||
{/* Folder Assignment */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-sm font-medium mb-3">Folder Assignment</h3>
|
||||
<h3 className="text-sm font-medium mb-3">{t('synthetic_users.filter_folder_assignment')}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
|
|
@ -1840,7 +1841,7 @@ const SyntheticUsers = () => {
|
|||
htmlFor="folderStatus-hasFolder"
|
||||
className="truncate overflow-hidden"
|
||||
>
|
||||
Has folder assignment
|
||||
{t('synthetic_users.filter_has_folder')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
|
@ -1853,7 +1854,7 @@ const SyntheticUsers = () => {
|
|||
htmlFor="folderStatus-noFolder"
|
||||
className="truncate overflow-hidden"
|
||||
>
|
||||
No folder assignment
|
||||
{t('synthetic_users.filter_no_folder')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1872,13 +1873,13 @@ const SyntheticUsers = () => {
|
|||
<div className="sticky bottom-0 bg-background border-t shadow-[0_-2px_4px_rgba(0,0,0,0.05)] pt-4 z-10">
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="outline"
|
||||
onClick={handleResetFilters}
|
||||
>
|
||||
Reset
|
||||
{t('synthetic_users.btn_reset_filters')}
|
||||
</Button>
|
||||
<Button onClick={applyFilters}>
|
||||
Apply Filters
|
||||
{t('synthetic_users.btn_apply_filters')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
|
|
@ -1892,9 +1893,9 @@ const SyntheticUsers = () => {
|
|||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select AI Model for Summary Generation</DialogTitle>
|
||||
<DialogTitle>{t('synthetic_users.dialog_select_model_title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Choose which AI model to use for generating persona summaries
|
||||
{t('synthetic_users.dialog_select_model_desc')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
|
@ -1907,27 +1908,27 @@ const SyntheticUsers = () => {
|
|||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="gpt-5.4" id="download-gpt54" />
|
||||
<Label htmlFor="download-gpt54" className="text-sm font-medium">
|
||||
GPT-5.4 (Recommended)
|
||||
{t('focus_group_setup.model_gpt54')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="gpt-5.4-mini" id="download-gpt54mini" />
|
||||
<Label htmlFor="download-gpt54mini" className="text-sm font-medium">
|
||||
GPT-5.4 Mini (Faster, lower cost)
|
||||
{t('focus_group_setup.model_gpt54_mini')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDownloadLlmModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
{t('synthetic_users.btn_cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleDownloadWithModel}>
|
||||
Generate Summary
|
||||
{t('synthetic_users.btn_generate_summary')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
|
@ -1938,8 +1939,8 @@ const SyntheticUsers = () => {
|
|||
) : (
|
||||
<Tabs defaultValue="ai" onValueChange={(value) => setCreationMode(value as 'manual' | 'ai')}>
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="ai">AI Recruiter</TabsTrigger>
|
||||
<TabsTrigger value="manual">Manual Creation</TabsTrigger>
|
||||
<TabsTrigger value="ai">{t('synthetic_users.tab_ai_recruiter')}</TabsTrigger>
|
||||
<TabsTrigger value="manual">{t('synthetic_users.tab_manual_creation')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="ai">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue