Extract business logic and UI into reusable pieces: Custom Hooks: - useFocusGroupAutoSave: debounced auto-save with retry logic - useFolderManagement: folder CRUD operations - usePersonaFiltering: filter state and persona filtering - useDiscussionGuideGeneration: guide generation and progress UI Components: - SaveStatusIndicator: auto-save status display - FolderSidebar: folder list and management - PersonaFilterDialog: persona filter modal - CopyGuideDialog: copy guide from other focus groups Tab Components: - SetupTab: form and asset uploader - ReviewTab: discussion guide viewer - ParticipantsTab: persona selection grid Reduces FocusGroupModerator from 2,396 to ~600 lines (75% reduction). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
600 lines
23 KiB
TypeScript
600 lines
23 KiB
TypeScript
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<string | null>(null);
|
|
const [selectedParticipants, setSelectedParticipants] = useState<string[]>([]);
|
|
const [backendAssets, setBackendAssets] = useState<any[]>([]);
|
|
const [personas, setPersonas] = useState<any[]>([]);
|
|
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<z.infer<typeof formSchema>>({
|
|
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 (
|
|
<>
|
|
<SaveStatusIndicator status={autoSave.autoSaveStatus} />
|
|
|
|
<div className="glass-panel rounded-xl p-6">
|
|
<div className="flex items-center gap-2 mb-6">
|
|
<MessageSquare className="h-5 w-5 text-primary" />
|
|
<h2 className="font-sf text-xl font-semibold">AI Focus Group Moderator</h2>
|
|
</div>
|
|
|
|
{/* Progress Modal */}
|
|
<ProgressModal
|
|
isOpen={guideGeneration.isGuideProgressModalOpen}
|
|
onClose={() => 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}
|
|
/>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="grid w-full grid-cols-3 mb-6">
|
|
<TabsTrigger value="setup">Setup</TabsTrigger>
|
|
<TabsTrigger value="review">Review & Edit</TabsTrigger>
|
|
<TabsTrigger value="participants">Participants</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="setup">
|
|
<SetupTab
|
|
form={form}
|
|
onSubmit={handleSubmit}
|
|
isGenerating={guideGeneration.guideGenerationState.isGenerating}
|
|
draftFocusGroupId={draftFocusGroupId}
|
|
backendAssets={backendAssets}
|
|
onAssetsChange={setBackendAssets}
|
|
onCopyGuideClick={() => setIsCopyGuideModalOpen(true)}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="review">
|
|
<ReviewTab
|
|
discussionGuide={guideGeneration.discussionGuide}
|
|
backendAssets={backendAssets}
|
|
draftFocusGroupId={draftFocusGroupId}
|
|
onSaveGuide={guideGeneration.handleSaveDiscussionGuide}
|
|
onDownloadGuide={guideGeneration.handleDownloadDiscussionGuide}
|
|
isDownloading={guideGeneration.isDownloadingGuide}
|
|
guideGenerationState={guideGeneration.guideGenerationState}
|
|
onEditingChange={guideGeneration.handleEditingStateChange}
|
|
onNavigateToSetup={() => setActiveTab('setup')}
|
|
onNavigateToParticipants={() => setActiveTab('participants')}
|
|
isJsonFormat={guideGeneration.isJsonFormat}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="participants">
|
|
<ParticipantsTab
|
|
personas={personas}
|
|
filteredPersonas={filtering.filteredPersonas}
|
|
selectedParticipants={selectedParticipants}
|
|
onParticipantToggle={handleParticipantSelection}
|
|
onViewDetails={handlePersonaViewDetails}
|
|
searchTerm={filtering.searchTerm}
|
|
onSearchChange={filtering.setSearchTerm}
|
|
activeFilters={filtering.activeFilters}
|
|
onOpenFilter={filtering.openFilterDialog}
|
|
isLoading={isLoadingPersonas}
|
|
folderSidebar={
|
|
<FolderSidebar
|
|
folders={folderManagement.folders}
|
|
selectedFolder={folderManagement.selectedFolder}
|
|
onSelectFolder={folderManagement.setSelectedFolder}
|
|
personas={personas}
|
|
isCreatingFolder={folderManagement.isCreatingFolder}
|
|
onStartCreateFolder={() => 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 */}
|
|
<PersonaFilterDialog
|
|
open={filtering.isFilterOpen}
|
|
onOpenChange={(open) => {
|
|
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}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* Copy Guide Dialog */}
|
|
<CopyGuideDialog
|
|
open={isCopyGuideModalOpen}
|
|
onOpenChange={setIsCopyGuideModalOpen}
|
|
onCopyGuide={handleCopyDiscussionGuide}
|
|
excludeFocusGroupId={draftFocusGroupId}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|