import { useState } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Loader2, Sparkles, ArrowRight, Check, Wand2, ShieldCheck, Trash2, Package } from 'lucide-react'; import { analyzeQuestionText, analyzeQuestionTargets, analyzeQuestionDeletions, analyzeQuestionProps, TextAnalysis, TargetAnalysis, DeletionAnalysis, PropsAnalysis, } from '@/lib/openai'; import { adminData } from '@/lib/adminApi'; import { auditedAdminData } from '@/hooks/useAuditedAdminData'; import { toast } from 'sonner'; type QuestionType = 'swipe' | 'text_answer' | 'audio' | 'photo' | 'who_likely'; const QUESTION_TYPE_LABELS: Record = { swipe: 'Swipe', text_answer: 'Text Answer', audio: 'Audio', photo: 'Photo', who_likely: 'Who Is More Likely', }; interface Question { id: string; text: string; partner_text: string | null; intensity: number; allowed_couple_genders: string[] | null; target_user_genders: string[] | null; required_props?: string[] | null; question_type?: QuestionType | null; } interface ReviewQuestionsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; questions: Question[]; onUpdated: () => void; } interface CombinedSuggestion { questionId: string; question: Question; textSuggestion?: TextAnalysis; targetSuggestion?: TargetAnalysis; propsSuggestion?: PropsAnalysis; deleteSuggestion?: DeletionAnalysis; } export function ReviewQuestionsDialog({ open, onOpenChange, questions, onUpdated }: ReviewQuestionsDialogProps) { const [analyzing, setAnalyzing] = useState(false); const [suggestions, setSuggestions] = useState([]); const [existingProps, setExistingProps] = useState([]); const [selectedTextIds, setSelectedTextIds] = useState>(new Set()); const [selectedTargetIds, setSelectedTargetIds] = useState>(new Set()); const [selectedPropsIds, setSelectedPropsIds] = useState>(new Set()); const [selectedDeleteIds, setSelectedDeleteIds] = useState>(new Set()); const [step, setStep] = useState<'initial' | 'review'>('initial'); const [applying, setApplying] = useState(false); // Helper functions for target comparison const isAllCoupleTargets = (targets: string[] | null): boolean => { if (!targets || targets.length === 0) return true; if (targets.length === 3) { const sorted = [...targets].sort(); return sorted[0] === 'female+female' && sorted[1] === 'female+male' && sorted[2] === 'male+male'; } return false; }; const isAllInitiator = (targets: string[] | null): boolean => { return !targets || targets.length === 0; }; const arraysEqual = (a: string[] | null, b: string[] | null): boolean => { if (!a && !b) return true; if (!a || !b) return false; if (a.length !== b.length) return false; const sortedA = [...a].sort(); const sortedB = [...b].sort(); return sortedA.every((val, i) => val === sortedB[i]); }; const loadExistingProps = async (): Promise => { if (existingProps.length > 0) return existingProps; try { const { data, error } = await adminData .from('questions') .select('required_props') .not('required_props', 'is', null) .limit(5000); if (error) throw error; const next = new Set(); data?.forEach(row => { row.required_props?.forEach((prop: string) => next.add(prop)); }); const list = Array.from(next).sort((a, b) => a.localeCompare(b)); setExistingProps(list); return list; } catch (error) { console.error('Failed to load existing props:', error); return []; } }; const handleAnalyze = async () => { setAnalyzing(true); try { const propsCatalog = await loadExistingProps(); // Run analyses in parallel (pass intensity for text analysis) const [textResults, targetResults, deleteResults, propsResults] = await Promise.all([ analyzeQuestionText( questions.map(q => ({ ...q, intensity: q.intensity })), false ), analyzeQuestionTargets(questions), analyzeQuestionDeletions(questions, false), analyzeQuestionProps( questions.map(q => ({ id: q.id, text: q.text, partner_text: q.partner_text, required_props: q.required_props || null, })), propsCatalog ), ]) as [TextAnalysis[], TargetAnalysis[], DeletionAnalysis[], PropsAnalysis[]]; // Filter target results to only meaningful changes const meaningfulTargetResults = targetResults.filter(suggestion => { const question = questions.find(q => q.id === suggestion.id); if (!question) return false; const currentCoupleIsAll = isAllCoupleTargets(question.allowed_couple_genders); const suggestedCoupleIsAll = isAllCoupleTargets(suggestion.suggested_targets); const coupleChanged = !(currentCoupleIsAll && suggestedCoupleIsAll) && !arraysEqual(question.allowed_couple_genders, suggestion.suggested_targets); const currentInitiatorIsAll = isAllInitiator(question.target_user_genders); const suggestedInitiatorIsAll = isAllInitiator(suggestion.suggested_initiator); const initiatorChanged = !(currentInitiatorIsAll && suggestedInitiatorIsAll) && !arraysEqual(question.target_user_genders, suggestion.suggested_initiator); return coupleChanged || initiatorChanged; }); const meaningfulPropsResults = propsResults.filter(suggestion => { const question = questions.find(q => q.id === suggestion.id); if (!question) return false; return !arraysEqual( question.required_props || null, suggestion.suggested_required_props || null ); }); const combined: CombinedSuggestion[] = questions.map(question => ({ questionId: question.id, question, textSuggestion: textResults.find(t => t.id === question.id), targetSuggestion: meaningfulTargetResults.find(t => t.id === question.id), propsSuggestion: meaningfulPropsResults.find(p => p.id === question.id), deleteSuggestion: deleteResults.find(d => d.id === question.id), })); setSuggestions(combined); // Default select all setSelectedTextIds(new Set(textResults.map(r => r.id))); setSelectedTargetIds(new Set(meaningfulTargetResults.map(r => r.id))); setSelectedPropsIds(new Set(meaningfulPropsResults.map(r => r.id))); setSelectedDeleteIds(new Set(deleteResults.map(r => r.id))); setStep('review'); } catch (error) { console.error(error); toast.error('Failed to analyze questions'); } finally { setAnalyzing(false); } }; const handleApply = async () => { setApplying(true); try { const updates: { id: string; data: Record }[] = []; const deleteIds = Array.from(selectedDeleteIds); // Build update objects combining text, target, and props changes suggestions.forEach(suggestion => { if (selectedDeleteIds.has(suggestion.questionId)) return; const textSelected = selectedTextIds.has(suggestion.questionId) && suggestion.textSuggestion; const targetSelected = selectedTargetIds.has(suggestion.questionId) && suggestion.targetSuggestion; const propsSelected = selectedPropsIds.has(suggestion.questionId) && suggestion.propsSuggestion; if (textSelected || targetSelected || propsSelected) { const updateData: Record = {}; if (textSelected && suggestion.textSuggestion) { updateData.text = suggestion.textSuggestion.suggested_text; updateData.partner_text = suggestion.textSuggestion.suggested_partner_text; } if (targetSelected && suggestion.targetSuggestion) { updateData.allowed_couple_genders = suggestion.targetSuggestion.suggested_targets && suggestion.targetSuggestion.suggested_targets.length > 0 ? suggestion.targetSuggestion.suggested_targets : null; updateData.target_user_genders = suggestion.targetSuggestion.suggested_initiator && suggestion.targetSuggestion.suggested_initiator.length > 0 ? suggestion.targetSuggestion.suggested_initiator : null; } if (propsSelected && suggestion.propsSuggestion) { updateData.required_props = suggestion.propsSuggestion.suggested_required_props && suggestion.propsSuggestion.suggested_required_props.length > 0 ? suggestion.propsSuggestion.suggested_required_props : null; } if (Object.keys(updateData).length > 0) { updates.push({ id: suggestion.questionId, data: updateData }); } } }); if (updates.length === 0 && deleteIds.length === 0) { onOpenChange(false); return; } // Apply updates/deletions in parallel with audit logging const operations: Promise[] = updates.map(update => auditedAdminData.update('questions', update.id, update.data) ); if (deleteIds.length > 0) { operations.push( auditedAdminData.deleteMany('questions', deleteIds).then(result => { if (result.error) throw result.error; }) ); } await Promise.all(operations); const updateMessage = updates.length > 0 ? `Updated ${updates.length} questions` : null; const deleteMessage = deleteIds.length > 0 ? `Deleted ${deleteIds.length} questions` : null; const message = [updateMessage, deleteMessage].filter(Boolean).join('. '); if (message) { toast.success(message); } onUpdated(); handleClose(); } catch (error) { console.error(error); toast.error('Failed to apply updates'); } finally { setApplying(false); } }; const handleClose = () => { onOpenChange(false); setTimeout(() => { setStep('initial'); setSuggestions([]); setSelectedTextIds(new Set()); setSelectedTargetIds(new Set()); setSelectedPropsIds(new Set()); setSelectedDeleteIds(new Set()); }, 300); }; const toggleTextSelection = (id: string) => { const next = new Set(selectedTextIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedTextIds(next); }; const toggleTargetSelection = (id: string) => { const next = new Set(selectedTargetIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedTargetIds(next); }; const togglePropsSelection = (id: string) => { const next = new Set(selectedPropsIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedPropsIds(next); }; const toggleDeleteSelection = (id: string) => { const next = new Set(selectedDeleteIds); if (next.has(id)) next.delete(id); else next.add(id); setSelectedDeleteIds(next); }; const formatTargets = (targets: string[] | null) => { if (!targets || targets.length === 0) return 'All'; return targets.map(t => t.replace('male+male', 'M+M').replace('female+male', 'M+F').replace('female+female', 'F+F')).join(', '); }; const formatInitiator = (targets: string[] | null) => { if (!targets || targets.length === 0) return 'Any'; return targets.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(', '); }; const formatProps = (props: string[] | null | undefined) => { if (!props || props.length === 0) return 'None'; return props.join(', '); }; const formatDeleteCategory = (category?: DeletionAnalysis['category']) => { switch (category) { case 'duplicate': return 'Duplicate'; case 'redundant': return 'Redundant'; case 'off-tone': return 'Off tone'; case 'unsafe': return 'Unsafe'; case 'too-vague': return 'Too vague'; case 'broken': return 'Broken'; case 'off-topic': return 'Off topic'; default: return 'Delete'; } }; const getQuestionTextById = (id?: string | null) => { if (!id) return null; const match = questions.find(q => q.id === id); return match ? match.text : null; }; // Count suggestions by type const textSuggestionCount = suggestions.filter(s => s.textSuggestion).length; const targetSuggestionCount = suggestions.filter(s => s.targetSuggestion).length; const propsSuggestionCount = suggestions.filter(s => s.propsSuggestion).length; const deleteSuggestionCount = suggestions.filter(s => s.deleteSuggestion).length; const effectiveTextCount = Array.from(selectedTextIds).filter(id => !selectedDeleteIds.has(id)).length; const effectiveTargetCount = Array.from(selectedTargetIds).filter(id => !selectedDeleteIds.has(id)).length; const effectivePropsCount = Array.from(selectedPropsIds).filter(id => !selectedDeleteIds.has(id)).length; const totalSelectedCount = effectiveTextCount + effectiveTargetCount + effectivePropsCount + selectedDeleteIds.size; const hasSuggestions = textSuggestionCount > 0 || targetSuggestionCount > 0 || propsSuggestionCount > 0 || deleteSuggestionCount > 0; // Select all/none helpers const selectAllText = () => setSelectedTextIds(new Set(suggestions.filter(s => s.textSuggestion).map(s => s.questionId))); const selectNoneText = () => setSelectedTextIds(new Set()); const selectAllTargets = () => setSelectedTargetIds(new Set(suggestions.filter(s => s.targetSuggestion).map(s => s.questionId))); const selectNoneTargets = () => setSelectedTargetIds(new Set()); const selectAllProps = () => setSelectedPropsIds(new Set(suggestions.filter(s => s.propsSuggestion).map(s => s.questionId))); const selectNoneProps = () => setSelectedPropsIds(new Set()); const selectAllDeletes = () => setSelectedDeleteIds(new Set(suggestions.filter(s => s.deleteSuggestion).map(s => s.questionId))); const selectNoneDeletes = () => setSelectedDeleteIds(new Set()); const selectAll = () => { selectAllText(); selectAllTargets(); selectAllProps(); selectAllDeletes(); }; const selectNone = () => { selectNoneText(); selectNoneTargets(); selectNoneProps(); selectNoneDeletes(); }; return ( Review Questions AI-powered analysis to improve phrasing, identify props, fix targeting, and suggest deletions.
{step === 'initial' ? (

Ready to analyze {questions.length} questions

The AI will analyze all questions and suggest improvements or removals for:

Text Phrasing
Audience Targeting
Required Props
Deletion Suggestions
) : (
{!hasSuggestions ? (
All questions look great! No improvements needed.
) : ( <> {/* Summary and bulk actions */}
{textSuggestionCount} text fixes {targetSuggestionCount} target fixes {propsSuggestionCount} props fixes {deleteSuggestionCount} delete suggestions
Question
Text Fix
Target Fix
Props
Delete {deleteSuggestionCount > 0 && ( )}
{suggestions.map((suggestion) => ( {/* Question Column */}
{suggestion.question.text}
{QUESTION_TYPE_LABELS[suggestion.question.question_type ?? 'swipe']}
{suggestion.question.partner_text && (
Partner: {suggestion.question.partner_text}
)}
{/* Text Fix Column */} {suggestion.textSuggestion ? (
toggleTextSelection(suggestion.questionId)} >
toggleTextSelection(suggestion.questionId)} className="mt-0.5" />
{suggestion.textSuggestion.suggested_text}
{suggestion.textSuggestion.suggested_partner_text && (
Partner: {suggestion.textSuggestion.suggested_partner_text}
)}
{suggestion.textSuggestion.reason}
) : (
No changes needed
)}
{/* Target Fix Column */} {suggestion.targetSuggestion ? (
toggleTargetSelection(suggestion.questionId)} >
toggleTargetSelection(suggestion.questionId)} className="mt-0.5" />
{/* Couple targets */}
Couples: {formatTargets(suggestion.question.allowed_couple_genders)} {formatTargets(suggestion.targetSuggestion.suggested_targets)}
{/* Initiator */}
Initiator: {formatInitiator(suggestion.question.target_user_genders)} {formatInitiator(suggestion.targetSuggestion.suggested_initiator)}
{suggestion.targetSuggestion.reason}
) : (
No changes needed
)}
{/* Props Column */} {suggestion.propsSuggestion ? (
togglePropsSelection(suggestion.questionId)} >
togglePropsSelection(suggestion.questionId)} className="mt-0.5" />
Current: {formatProps(suggestion.question.required_props)} {formatProps(suggestion.propsSuggestion.suggested_required_props)}
{suggestion.propsSuggestion.reason}
) : (
No changes needed
)}
{/* Delete Column */} {suggestion.deleteSuggestion ? (
toggleDeleteSelection(suggestion.questionId)} >
toggleDeleteSelection(suggestion.questionId)} className="mt-0.5" />
{formatDeleteCategory(suggestion.deleteSuggestion.category)} recommended
{suggestion.deleteSuggestion.duplicate_of_id && (
Duplicate of: {getQuestionTextById(suggestion.deleteSuggestion.duplicate_of_id) || 'Another question'}
)}
{suggestion.deleteSuggestion.reason}
) : (
Keep
)}
))}
)}
)}
{step === 'review' && ( <> )}
); }