import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigation } from '@/contexts/NavigationContext'; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { MessageSquare } from 'lucide-react'; import { toast } from 'sonner'; import { personasApi, focusGroupsApi } from '@/lib/api'; import ProgressModal from '@/components/ui/ProgressModal'; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Persona } from "@/types/persona"; // Custom hooks import { useFocusGroupAutoSave } from '@/hooks/useFocusGroupAutoSave'; import { useFolderManagement } from '@/hooks/useFolderManagement'; import { usePersonaFiltering } from '@/hooks/usePersonaFiltering'; import { useDiscussionGuideGeneration } from '@/hooks/useDiscussionGuideGeneration'; // Components import { SaveStatusIndicator } from '@/components/ui/SaveStatusIndicator'; import { FolderSidebar } from '@/components/focus-group-session/FolderSidebar'; import { PersonaFilterDialog } from '@/components/focus-group-session/PersonaFilterDialog'; import { CopyGuideDialog } from '@/components/focus-group-session/CopyGuideDialog'; import { SetupTab } from '@/components/focus-group-session/SetupTab'; import { ReviewTab } from '@/components/focus-group-session/ReviewTab'; import { ParticipantsTab } from '@/components/focus-group-session/ParticipantsTab'; // Form schema const formSchema = z.object({ researchBrief: z.string().min(10, { message: "Research brief must be at least 10 characters.", }), focusGroupName: z.string().min(3, { message: "Focus group name must be at least 3 characters.", }), discussionTopics: z.string().min(10, { message: "Discussion topics are required.", }), duration: z.string().min(1, { message: "Duration is required.", }), llm_model: z.string().optional(), reasoning_effort: z.string().optional(), verbosity: z.string().optional(), }); interface FocusGroupModeratorProps { draftToEdit?: any | null; onDraftSaved?: () => void; preSelectedParticipants?: string[]; } export default function FocusGroupModerator({ draftToEdit, onDraftSaved, preSelectedParticipants = [] }: FocusGroupModeratorProps = {}) { const navigate = useNavigate(); const { setPreviousRoute, navigationState, clearNavigationState } = useNavigation(); // Tab state const [activeTab, setActiveTab] = useState('setup'); // Core state const [draftFocusGroupId, setDraftFocusGroupId] = useState(null); const [selectedParticipants, setSelectedParticipants] = useState([]); const [backendAssets, setBackendAssets] = useState([]); const [personas, setPersonas] = useState([]); const [isLoadingPersonas, setIsLoadingPersonas] = useState(false); // Copy guide dialog state const [isCopyGuideModalOpen, setIsCopyGuideModalOpen] = useState(false); // Track if draft has been loaded const draftLoadedRef = useRef(false); // Initialize form const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { researchBrief: "", focusGroupName: "", discussionTopics: "", duration: "60", llm_model: "gemini-3-pro-preview", reasoning_effort: "medium", verbosity: "medium", }, }); // Custom hooks const folderManagement = useFolderManagement(); const filtering = usePersonaFiltering({ personas, selectedFolder: folderManagement.selectedFolder, }); const guideGeneration = useDiscussionGuideGeneration({ form, }); const autoSave = useFocusGroupAutoSave({ form, selectedParticipants, backendAssets, draftFocusGroupId, draftToEdit, activeTab, onDraftIdChange: setDraftFocusGroupId, }); // Fetch personas on mount useEffect(() => { const fetchPersonas = async () => { setIsLoadingPersonas(true); try { const response = await personasApi.getAll(); if (Array.isArray(response.data) && response.data.length > 0) { setPersonas(response.data); } else { toast.warning("No participants available"); } } catch (error) { console.error("Error fetching personas:", error); toast.error("Failed to load participants"); } finally { setIsLoadingPersonas(false); } }; fetchPersonas(); }, []); // Fetch backend assets const fetchBackendAssets = useCallback(async (focusGroupId: string) => { try { const response = await focusGroupsApi.getAssets(focusGroupId); setBackendAssets(response.data.assets || []); } catch (error) { console.error("Error fetching backend assets:", error); toast.error("Failed to load asset information"); } }, []); // Load draft data when editing useEffect(() => { if (!draftToEdit) { draftLoadedRef.current = false; return; } if (draftToEdit && !draftLoadedRef.current) { autoSave.setIsLoadingDraft(true); draftLoadedRef.current = true; const draftId = draftToEdit.id || draftToEdit._id; setDraftFocusGroupId(draftId); if (draftId) { fetchBackendAssets(draftId); } // Load form data if (draftToEdit.name) form.setValue('focusGroupName', draftToEdit.name); if (draftToEdit.description || draftToEdit.objective) { form.setValue('researchBrief', draftToEdit.description || draftToEdit.objective || ''); } if (draftToEdit.topic) form.setValue('discussionTopics', draftToEdit.topic); if (draftToEdit.duration) form.setValue('duration', draftToEdit.duration.toString()); if (draftToEdit.llm_model) form.setValue('llm_model', draftToEdit.llm_model); if (draftToEdit.reasoning_effort) form.setValue('reasoning_effort', draftToEdit.reasoning_effort); if (draftToEdit.verbosity) form.setValue('verbosity', draftToEdit.verbosity); // Load discussion guide if (draftToEdit.discussionGuide) { guideGeneration.setDiscussionGuide(draftToEdit.discussionGuide); if (!navigationState.focusGroupTab || navigationState.previousRoute !== '/focus-groups') { setActiveTab('review'); } } // Load participants if (draftToEdit.participants && Array.isArray(draftToEdit.participants)) { setSelectedParticipants(draftToEdit.participants); } // Set last saved data autoSave.setLastSavedData({ name: draftToEdit.name || '', description: draftToEdit.description || draftToEdit.objective || '', objective: draftToEdit.description || draftToEdit.objective || '', topic: draftToEdit.topic || '', duration: draftToEdit.duration || 60, llm_model: draftToEdit.llm_model || 'gemini-3-pro-preview', reasoning_effort: draftToEdit.reasoning_effort || 'medium', verbosity: draftToEdit.verbosity || 'medium', participants: draftToEdit.participants || [], participants_count: (draftToEdit.participants || []).length, status: 'draft', date: draftToEdit.date || new Date().toISOString(), uploadedAssets: backendAssets.map(a => a.filename || a.original_name || 'unknown') }); toast.success("Draft focus group loaded", { description: "Continue editing your focus group setup" }); setTimeout(() => { autoSave.setIsLoadingDraft(false); }, 1000); } }, [draftToEdit, form, fetchBackendAssets, navigationState, autoSave, guideGeneration, backendAssets]); // Handle pre-selected participants useEffect(() => { if (preSelectedParticipants.length > 0) { setSelectedParticipants(preSelectedParticipants); setActiveTab('participants'); } }, [preSelectedParticipants]); // Handle navigation state for returning from persona details useEffect(() => { if (navigationState.focusGroupTab && navigationState.previousRoute === '/focus-groups') { setTimeout(() => { setActiveTab(navigationState.focusGroupTab); clearNavigationState(); }, 0); } }, [navigationState.focusGroupTab, draftToEdit, clearNavigationState]); // Revert to setup tab when generation is cancelled useEffect(() => { if (!guideGeneration.guideGenerationState.isGenerating && !guideGeneration.guideGenerationState.isCancelling && guideGeneration.guideGenerationState.taskId === null && activeTab === 'review' && !guideGeneration.discussionGuide) { setActiveTab('setup'); } }, [guideGeneration.guideGenerationState, activeTab, guideGeneration.discussionGuide]); // Handle participant selection const handleParticipantSelection = useCallback((id: string) => { setSelectedParticipants(prev => { const isSelected = prev.includes(id); return isSelected ? prev.filter(pId => pId !== id) : [...prev, id]; }); }, []); // Handle persona view details const handlePersonaViewDetails = useCallback((persona: Persona) => { setPreviousRoute('/focus-groups', { focusGroupId: draftFocusGroupId, focusGroupTab: 'participants', isNewFocusGroup: !draftToEdit, focusGroupData: { name: form.getValues('focusGroupName'), description: form.getValues('researchBrief'), selectedParticipants: selectedParticipants, discussionGuide: guideGeneration.discussionGuide, } }); navigate(`/synthetic-users/${persona.id}`); }, [setPreviousRoute, draftFocusGroupId, draftToEdit, form, selectedParticipants, guideGeneration.discussionGuide, navigate]); // Handle form submission (generate discussion guide) const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); const isValid = await form.trigger(); if (!isValid) return; const values = form.getValues(); try { let focusGroupId = draftFocusGroupId; if (!focusGroupId) { const draftData = { name: values.focusGroupName, status: 'draft', participants: selectedParticipants, participants_count: selectedParticipants.length, date: new Date().toISOString(), duration: parseInt(values.duration), topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'), description: values.researchBrief, objective: values.researchBrief, llm_model: values.llm_model, reasoning_effort: values.reasoning_effort, verbosity: values.verbosity, }; const savedDraft = await focusGroupsApi.create(draftData); focusGroupId = savedDraft.data.focus_group_id || savedDraft.data.id || savedDraft.data._id; setDraftFocusGroupId(focusGroupId); } // Update focus group before generating if (focusGroupId) { const preUpdateData = { name: values.focusGroupName, participants: selectedParticipants, participants_count: selectedParticipants.length, duration: parseInt(values.duration), topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'), description: values.researchBrief, objective: values.researchBrief, llm_model: values.llm_model, reasoning_effort: values.reasoning_effort, verbosity: values.verbosity }; await focusGroupsApi.update(focusGroupId, preUpdateData); } // Generate discussion guide const guide = await guideGeneration.generateDiscussionGuide(values, focusGroupId); if (!guide || (typeof guide === 'string' && guide.trim() === '')) { return; // Cancelled } guideGeneration.setDiscussionGuide(guide); // Save with discussion guide if (focusGroupId) { const updateData = { name: values.focusGroupName, status: 'draft', participants: selectedParticipants, participants_count: selectedParticipants.length, date: new Date().toISOString(), duration: parseInt(values.duration), topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'), description: values.researchBrief, objective: values.researchBrief, llm_model: values.llm_model, reasoning_effort: values.reasoning_effort, verbosity: values.verbosity, discussionGuide: guide }; await focusGroupsApi.update(focusGroupId, updateData); toast.success("Progress saved as draft"); } setActiveTab('review'); toast.success("Discussion guide generated", { description: "Review and edit before proceeding", }); } catch (error: any) { console.error("Error in focus group creation flow:", error); toast.error("Discussion guide generation failed", { description: "Please go back to the setup tab and try generating again.", duration: 8000, }); } }, [draftFocusGroupId, form, selectedParticipants, guideGeneration]); // Handle copying discussion guide from another focus group const handleCopyDiscussionGuide = useCallback(async (sourceFocusGroup: any) => { if (!sourceFocusGroup || !sourceFocusGroup.discussionGuide) { toast.error("Selected focus group does not have a discussion guide"); return; } guideGeneration.setDiscussionGuide(sourceFocusGroup.discussionGuide); // Update draft with copied guide if (draftFocusGroupId) { try { const values = form.getValues(); const updateData = { name: values.focusGroupName, status: 'draft', participants: selectedParticipants, participants_count: selectedParticipants.length, duration: parseInt(values.duration || '60'), topic: values.discussionTopics || '', description: values.researchBrief || '', objective: values.researchBrief || '', llm_model: values.llm_model || 'gemini-3-pro-preview', reasoning_effort: values.reasoning_effort || 'medium', verbosity: values.verbosity || 'medium', discussionGuide: sourceFocusGroup.discussionGuide }; await focusGroupsApi.update(draftFocusGroupId, updateData); } catch (error) { console.error("Failed to update focus group with copied discussion guide:", error); toast.error("Failed to save copied discussion guide"); } } setIsCopyGuideModalOpen(false); setActiveTab('review'); toast.success("Discussion guide copied successfully", { description: `Copied from "${sourceFocusGroup.name}"`, }); }, [draftFocusGroupId, form, selectedParticipants, guideGeneration]); // Handle starting focus group const handleStartFocusGroup = useCallback(async () => { if (!form.getValues().focusGroupName) { toast.error("Missing focus group name"); return; } if (!guideGeneration.discussionGuide) { toast.error("Missing discussion guide"); return; } if (selectedParticipants.length < 1) { toast.error("Not enough participants", { description: "Please select at least one participant", }); return; } try { toast.loading("Creating focus group..."); const values = form.getValues(); let focusGroupId = draftFocusGroupId; if (focusGroupId) { const updatedData = { name: values.focusGroupName, status: 'in-progress', participants: selectedParticipants, participants_count: selectedParticipants.length, date: new Date().toISOString(), duration: parseInt(values.duration), topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'), description: values.researchBrief, objective: values.researchBrief, discussionGuide: guideGeneration.discussionGuide }; await focusGroupsApi.update(focusGroupId, updatedData); if (onDraftSaved) onDraftSaved(); } else { const focusGroupData = { name: values.focusGroupName, status: 'in-progress', participants: selectedParticipants, participants_count: selectedParticipants.length, date: new Date().toISOString(), duration: parseInt(values.duration), topic: values.discussionTopics.split(',')[0].trim().toLowerCase().replace(/\s+/g, '-'), discussionGuide: guideGeneration.discussionGuide }; const response = await focusGroupsApi.create(focusGroupData); focusGroupId = response.data.focus_group_id; } toast.dismiss(); toast.success("Focus group created successfully"); navigate(`/focus-groups/${focusGroupId}`); } catch (error) { toast.dismiss(); console.error("Failed to start focus group:", error); toast.error("Failed to create focus group"); } }, [form, guideGeneration.discussionGuide, selectedParticipants, draftFocusGroupId, onDraftSaved, navigate]); return ( <>

AI Focus Group Moderator

{/* Progress Modal */} guideGeneration.setIsGuideProgressModalOpen(false)} isActive={guideGeneration.guideGenerationState.isGenerating} isComplete={guideGeneration.guideGenerationState.isComplete} hasError={guideGeneration.guideGenerationState.hasError} isCancelling={guideGeneration.guideGenerationState.isCancelling} taskId={guideGeneration.guideGenerationState.taskId} title="Generating Discussion Guide" description="Creating your discussion guide based on the research objectives. This typically takes 30-60 seconds." onCancel={guideGeneration.guideGenerationControls.cancelGeneration} onComplete={guideGeneration.handleGuideProgressComplete} /> Setup Review & Edit Participants setIsCopyGuideModalOpen(true)} /> setActiveTab('setup')} onNavigateToParticipants={() => setActiveTab('participants')} isJsonFormat={guideGeneration.isJsonFormat} /> folderManagement.setIsCreatingFolder(true)} newFolderName={folderManagement.newFolderName} onNewFolderNameChange={folderManagement.setNewFolderName} onConfirmCreateFolder={folderManagement.createNewFolder} onCancelCreateFolder={folderManagement.cancelFolderCreation} folderToRename={folderManagement.folderToRename} renameFolderName={folderManagement.renameFolderName} onRenameFolderNameChange={folderManagement.setRenameFolderName} onStartRenameFolder={folderManagement.startRenameFolder} onConfirmRenameFolder={folderManagement.completeRenameFolder} onCancelRenameFolder={folderManagement.cancelRenameFolder} /> } onNavigateToReview={() => setActiveTab('review')} onStartFocusGroup={handleStartFocusGroup} canStart={selectedParticipants.length >= 1 && !!guideGeneration.discussionGuide} /> {/* Filter Dialog */} { if (open) { filtering.openFilterDialog(); } else { filtering.setIsFilterOpen(false); } }} workingFilters={filtering.workingFilters} personas={personas} getFilterOptions={filtering.getFilterOptions} getFilteredOptions={filtering.getFilteredOptions} onToggleFilter={filtering.toggleFilter} onApply={filtering.applyFilters} onReset={filtering.resetFilters} /> {/* Copy Guide Dialog */}
); }