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>
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import React from 'react';
|
|
import { UseFormReturn } from 'react-hook-form';
|
|
import {
|
|
MessageSquare,
|
|
Copy,
|
|
} from 'lucide-react';
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import AssetUploader from '@/components/AssetUploader';
|
|
|
|
interface SetupTabProps {
|
|
form: UseFormReturn<any>;
|
|
onSubmit: (e: React.FormEvent) => void;
|
|
isGenerating: boolean;
|
|
draftFocusGroupId: string | null;
|
|
backendAssets: any[];
|
|
onAssetsChange: (assets: any[]) => void;
|
|
onCopyGuideClick: () => void;
|
|
}
|
|
|
|
export function SetupTab({
|
|
form,
|
|
onSubmit,
|
|
isGenerating,
|
|
draftFocusGroupId,
|
|
backendAssets,
|
|
onAssetsChange,
|
|
onCopyGuideClick,
|
|
}: SetupTabProps) {
|
|
const selectedModel = form.watch("llm_model");
|
|
|
|
return (
|
|
<>
|
|
<Form {...form}>
|
|
<form onSubmit={onSubmit} className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="focusGroupName"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Focus Group Name</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="e.g., Mobile App UX Evaluation" {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
Give your focus group a descriptive name
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="researchBrief"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Research Brief</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="Describe your research objectives..."
|
|
className="h-36"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Provide context about what you want to learn
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="space-y-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="discussionTopics"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Discussion Topics</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder="List main topics to cover, separated by commas"
|
|
className="h-24"
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
E.g., User experience, feature preferences, pain points
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="duration"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Duration (minutes)</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select duration" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="30">30 minutes</SelectItem>
|
|
<SelectItem value="45">45 minutes</SelectItem>
|
|
<SelectItem value="60">60 minutes</SelectItem>
|
|
<SelectItem value="90">90 minutes</SelectItem>
|
|
<SelectItem value="120">120 minutes</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
How long should the focus group session last?
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="llm_model"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>AI Model</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select AI model" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="gemini-3-pro-preview">Gemini 3 Pro (Slow, best for most tasks)</SelectItem>
|
|
<SelectItem value="gpt-4.1">GPT-4.1 (Fast, best for speed)</SelectItem>
|
|
<SelectItem value="gpt-5">GPT-5 (Slow, best for complex tasks)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Choose which AI model to use for generating responses, discussion guides, and thematic analysis
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{/* GPT-5 specific parameters */}
|
|
{selectedModel === "gpt-5" && (
|
|
<>
|
|
<FormField
|
|
control={form.control}
|
|
name="reasoning_effort"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Reasoning Effort</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select reasoning effort" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="minimal">Minimal - Fast responses</SelectItem>
|
|
<SelectItem value="low">Low - Quick thinking</SelectItem>
|
|
<SelectItem value="medium">Medium - Balanced (default)</SelectItem>
|
|
<SelectItem value="high">High - Deep reasoning</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Controls how much time GPT-5 spends thinking before responding
|
|
</FormDescription>
|
|
<div className="text-xs text-amber-600 font-medium mt-1">
|
|
Controls how much time GPT-5 spends thinking before responding
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="verbosity"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Response Verbosity</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select verbosity level" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="low">Low - Concise responses</SelectItem>
|
|
<SelectItem value="medium">Medium - Balanced length (default)</SelectItem>
|
|
<SelectItem value="high">High - Detailed responses</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Controls how detailed and lengthy GPT-5's responses will be
|
|
</FormDescription>
|
|
<div className="text-xs text-amber-600 font-medium mt-1">
|
|
Controls how much time GPT-5 spends thinking before responding
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stimulus Uploader */}
|
|
<div>
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-2 block">
|
|
Upload Your Stimulus (if any)
|
|
</label>
|
|
<AssetUploader
|
|
focusGroupId={draftFocusGroupId}
|
|
disabled={!draftFocusGroupId}
|
|
onUploadComplete={(assets) => {
|
|
onAssetsChange(assets);
|
|
}}
|
|
onUploadError={(error) => {
|
|
console.error('Asset upload error:', error);
|
|
}}
|
|
onAssetsChange={(assets) => {
|
|
onAssetsChange(assets);
|
|
}}
|
|
maxAssets={10}
|
|
maxFileSize={10}
|
|
allowedTypes={['image/*', 'application/pdf', 'video/*', 'text/*', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']}
|
|
label="Upload Your Stimulus"
|
|
description="Provide any files you wish the moderator to use in the focus group session. This could include mockups, designs, documents or other materials for discussion. The moderator will write a discussion guide partially around these assets. You can edit this in the next step."
|
|
enableRenaming={true}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
|
|
{/* Action buttons outside of form to prevent form submission issues */}
|
|
<div className="flex justify-end gap-3 mt-6">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onCopyGuideClick();
|
|
}}
|
|
disabled={isGenerating}
|
|
className="min-w-32"
|
|
>
|
|
<Copy className="mr-2 h-4 w-4" />
|
|
Copy Discussion Guide
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
onClick={onSubmit}
|
|
disabled={isGenerating}
|
|
className="min-w-32"
|
|
>
|
|
<MessageSquare className="mr-2 h-4 w-4" />
|
|
{isGenerating ? "Generating..." : "Generate Discussion Guide"}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default SetupTab;
|