import { useState, useEffect } from 'react'; import { z } from "zod"; import { Users } from 'lucide-react'; import { toast } from 'sonner'; import { useLocation, useNavigate } from 'react-router-dom'; import { Progress } from "@/components/ui/progress"; import AIRecruiterForm, { formSchema } from './ai-recruiter/AIRecruiterForm'; import PersonaReviewList from './ai-recruiter/PersonaReviewList'; import { generateSyntheticPersonas } from '@/utils/personaGenerator'; import { usePersonaStorage, GENERATED_PERSONAS_KEY } from '@/hooks/usePersonaStorage'; import { Persona } from "@/types/persona"; interface AIRecruiterProps { targetFolderId?: string | null; targetFolderName?: string | null; } export default function AIRecruiter({ targetFolderId, targetFolderName }: AIRecruiterProps) { const location = useLocation(); const navigate = useNavigate(); const { loadPersonas, savePersonas } = usePersonaStorage(); const [isGenerating, setIsGenerating] = useState(false); const [generatedPersonas, setGeneratedPersonas] = useState([]); const [selectedPersonas, setSelectedPersonas] = useState([]); const [showReview, setShowReview] = useState(false); const [generationProgress, setGenerationProgress] = useState(0); // Check URL params for state restoration useEffect(() => { const searchParams = new URLSearchParams(location.search); const mode = searchParams.get('mode'); const tab = searchParams.get('tab'); const step = searchParams.get('step'); // If we're returning to the review step, restore the state if (mode === 'create' && tab === 'ai' && step === 'review') { const personas = loadPersonas(); if (personas.length > 0) { setGeneratedPersonas(personas); setSelectedPersonas(personas.map(p => p.id)); setShowReview(true); } } }, [location, loadPersonas]); async function onSubmit(values: z.infer) { try { setIsGenerating(true); setGenerationProgress(0); // Validate count before proceeding const count = parseInt(values.personaCount); if (isNaN(count) || count < 1 || count > 10) { toast.error("Invalid number of personas", { description: "Please enter a number between 1 and 10" }); setIsGenerating(false); return; } // Start progress animation setGenerationProgress(5); // Initial progress // Simulate progress while waiting for personas to generate const progressInterval = setInterval(() => { setGenerationProgress(prev => { // Increase gradually but never reach 100% until actually complete if (prev < 90) { return prev + Math.random() * 5; } return prev; }); }, 500); // Adjust the expected time based on the count const estimatedTime = count <= 2 ? "30-60 seconds" : count <= 4 ? "1-2 minutes" : count <= 6 ? "2-3 minutes" : "3-5 minutes"; // Warn about potential timeouts for larger counts if (count > 4) { toast.info("Generation may take longer", { description: `Generating ${count} personas at once may result in some timeouts. If this happens, the successfully created personas will still be saved.`, duration: 8000 }); } toast.info("Generating AI personas in parallel", { description: `Creating ${count} synthetic personas based on your brief. This may take ${estimatedTime}. Please be patient.`, duration: 10000 }); // Show folder info in toast if available if (targetFolderId && targetFolderName) { console.log(`Target folder for new personas: ID=${targetFolderId}, Name=${targetFolderName}`); toast.info(`Creating personas in "${targetFolderName}" folder`, { duration: 3000 }); } else { console.log("No target folder specified for new personas"); } // Log which model is being used for generation console.log(`🤖 Starting persona generation with model: ${values.llm_model || 'gemini-2.5-pro'}`); const response = await generateSyntheticPersonas( values.audienceBrief, values.researchObjective, count, values.dataFile, targetFolderId, values.llm_model ); // Extract personas from the response const personas = response.personas || response; // Clear the progress interval clearInterval(progressInterval); // Set progress to 100% when done setGenerationProgress(100); // Check for partial success (some personas generated, some failed) if (personas && personas.length > 0) { // Log successful generation with model info console.log(`✅ Successfully generated ${personas.length} personas using model: ${values.llm_model || 'gemini-2.5-pro'}`); // Check if we got a response with partial success info if (response.partial_success || (response.errors && response.errors.length > 0)) { // Some personas succeeded but others failed toast.success("Some personas generated successfully", { description: `${personas.length} synthetic personas were created using ${values.llm_model || 'Gemini 2.5 Pro'}. ${response.errors?.length || 0} failed due to timeout or other errors.`, duration: 8000 }); // Show details about the failures if (response.errors && response.errors.length > 0) { setTimeout(() => { toast.error("Some personas failed to generate", { description: `${response.errors.length} personas timed out. The server took too long to generate them. The successfully generated personas have been saved${targetFolderId ? ` in the selected folder` : ''}.`, duration: 10000 }); }, 1000); } } else { // All personas succeeded toast.success("Personas generated and saved successfully", { description: `${personas.length} synthetic personas have been created using ${values.llm_model || 'Gemini 2.5 Pro'} and saved ${targetFolderId ? `to the "${targetFolderName}" folder` : 'to the database'}.` }); } // Navigate directly back to synthetic users list navigate('/synthetic-users?mode=view'); } else { throw new Error("No personas were generated"); } } catch (error) { console.error(`❌ Error generating personas using model: ${values.llm_model || 'gemini-2.5-pro'}:`, error); let errorMessage = "Please try again or adjust your parameters"; let errorTitle = "Failed to generate personas"; // Check for specific error types if (error.code === "ECONNABORTED" || error.message?.includes("timeout") || error.response?.status === 504) { errorTitle = "Generation timeout"; errorMessage = "AI persona generation timed out. This often happens when generating multiple complex personas. Try generating fewer personas (2-3) or try again later."; } else if (error.response?.status === 500) { errorTitle = "Server error"; // Try to extract the error message from the response if available if (error.response?.data?.message) { errorMessage = error.response.data.message; } else if (error.response?.data?.error) { errorMessage = error.response.data.error; } else { errorMessage = "The server encountered an error processing your request. Please try again later."; } } else if (error.response?.status === 401) { errorTitle = "Authentication required"; errorMessage = "Please log in to generate personas."; } else if (error.message?.includes("504 Deadline Exceeded")) { errorTitle = "Generation timeout"; errorMessage = "The AI model took too long to generate personas. Try generating fewer personas or simplify your brief."; } else if (error instanceof Error) { errorMessage = error.message; } toast.error(errorTitle, { description: errorMessage, duration: 6000 // Show for longer to ensure user sees it }); } finally { // Wait a moment to show 100% completion before resetting setTimeout(() => { setIsGenerating(false); setGenerationProgress(0); }, 500); } } const handlePersonaSelection = (id: string) => { setSelectedPersonas(prev => prev.includes(id) ? prev.filter(pId => pId !== id) : [...prev, id] ); }; const processRefinementInstructions = (personas: Persona[], instructions: string) => { // This would connect to a real LLM in production // For demo purposes, we'll simulate some simple transformations const lowerInstructions = instructions.toLowerCase(); return personas.map(p => { const persona = { ...p }; // Handle age modifications if (lowerInstructions.includes('younger')) { const currentAge = parseInt(persona.age); persona.age = (currentAge - 5).toString(); // Make 5 years younger } else if (lowerInstructions.includes('older')) { const currentAge = parseInt(persona.age); persona.age = (currentAge + 5).toString(); // Make 5 years older } // Handle location diversity if (lowerInstructions.includes('different locations')) { // In a real implementation, this would be handled by the LLM // For demo, we'll just append a modifier to make it clear the change happened persona.location = `${persona.location} (Diversified)`; } // Handle personality traits if (lowerInstructions.includes('more extroverted')) { persona.personality = `Extroverted, ${persona.personality.toLowerCase()}`; } else if (lowerInstructions.includes('more introverted')) { persona.personality = `Introverted, ${persona.personality.toLowerCase()}`; } // Add some randomized modifications based on the instructions if (lowerInstructions.includes('diverse')) { // Randomize some traits const randomTraits = ['tech-savvy', 'traditional', 'innovative', 'conservative', 'creative']; const selectedTrait = randomTraits[Math.floor(Math.random() * randomTraits.length)]; persona.personality = `${selectedTrait}, ${persona.personality}`; } return persona; }); }; const handleRefinePersonas = (refinementPrompt: string) => { if (!refinementPrompt.trim()) { toast.error("Please provide refinement instructions"); return; } setIsGenerating(true); // In a production environment, this would call an actual LLM API setTimeout(() => { try { const selectedPersonaObjects = generatedPersonas.filter(p => selectedPersonas.includes(p.id) ); const refinedSelected = processRefinementInstructions( selectedPersonaObjects, refinementPrompt ); // Replace only the selected personas with their refined versions const updatedPersonas = generatedPersonas.map(p => { const refined = refinedSelected.find(r => r.id === p.id); return refined || p; }); setGeneratedPersonas(updatedPersonas); setIsGenerating(false); savePersonas(updatedPersonas); toast.success("Personas refined based on your instructions", { description: "Review the updated profiles", }); } catch (error) { console.error("Error refining personas:", error); toast.error("Failed to refine personas", { description: "Please try different instructions", }); setIsGenerating(false); } }, 1500); }; const handleApprovePersonas = () => { const approved = generatedPersonas.filter(p => selectedPersonas.includes(p.id)); toast.success(`${approved.length} personas approved`, { description: "Added to your synthetic persona library", }); savePersonas(approved); // Redirect to the persona library view instead of resetting navigate('/synthetic-users?mode=view'); }; return (

AI Persona Recruiter

{isGenerating && (
Generating personas in parallel... {Math.round(generationProgress)}%
)} {!showReview ? ( ) : ( setShowReview(false)} /> )}
); }