semblance-dev/src/components/focus-group-session/SetupTab.tsx
michael 22b3ec19a5 Refactor FocusGroupModerator into smaller components and hooks
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>
2025-12-04 09:11:21 -06:00

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;