Files
Sauci/apps/admin/src/components/content/ReviewQuestionsDialog.tsx
T
jjbrunton 4e40b92b16 Add MCP server and enhance AI content tools
Implement self-hosted MCP server for admin actions, update AI generation tools in admin dashboard, and refine mobile app stores and components.
2026-01-10 19:24:06 +00:00

864 lines
55 KiB
TypeScript

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, Gauge, Trash2, Package } from 'lucide-react';
import {
analyzeQuestionText,
analyzeQuestionTargets,
analyzeQuestionDeletions,
analyzeQuestionProps,
TextAnalysis,
TargetAnalysis,
DeletionAnalysis,
PropsAnalysis,
} from '@/lib/openai';
import { supabase } from '@/config';
import { auditedSupabase } from '@/hooks/useAuditedSupabase';
import { toast } from 'sonner';
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;
}
interface ReviewQuestionsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
questions: Question[];
isExplicit?: boolean;
onUpdated: () => void;
}
interface CombinedSuggestion {
questionId: string;
question: Question;
textSuggestion?: TextAnalysis;
targetSuggestion?: TargetAnalysis;
propsSuggestion?: PropsAnalysis;
deleteSuggestion?: DeletionAnalysis;
}
export function ReviewQuestionsDialog({ open, onOpenChange, questions, isExplicit = false, onUpdated }: ReviewQuestionsDialogProps) {
const [analyzing, setAnalyzing] = useState(false);
const [suggestions, setSuggestions] = useState<CombinedSuggestion[]>([]);
const [existingProps, setExistingProps] = useState<string[]>([]);
const [selectedTextIds, setSelectedTextIds] = useState<Set<string>>(new Set());
const [selectedTargetIds, setSelectedTargetIds] = useState<Set<string>>(new Set());
const [selectedPropsIds, setSelectedPropsIds] = useState<Set<string>>(new Set());
const [selectedIntensityIds, setSelectedIntensityIds] = useState<Set<string>>(new Set());
const [selectedDeleteIds, setSelectedDeleteIds] = useState<Set<string>>(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<string[]> => {
if (existingProps.length > 0) return existingProps;
try {
const { data, error } = await supabase
.from('questions')
.select('required_props')
.not('required_props', 'is', null)
.limit(5000);
if (error) throw error;
const next = new Set<string>();
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 })),
isExplicit
),
analyzeQuestionTargets(questions),
analyzeQuestionDeletions(questions, isExplicit),
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)));
// Select intensity suggestions by default
setSelectedIntensityIds(new Set(
textResults.filter(r => r.suggested_intensity != null).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<string, unknown> }[] = [];
const deleteIds = Array.from(selectedDeleteIds);
// Build update objects combining text, target, and intensity 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;
const intensitySelected = selectedIntensityIds.has(suggestion.questionId) &&
suggestion.textSuggestion?.suggested_intensity != null;
if (textSelected || targetSelected || propsSelected || intensitySelected) {
const updateData: Record<string, unknown> = {};
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 (intensitySelected && suggestion.textSuggestion?.suggested_intensity != null) {
updateData.intensity = suggestion.textSuggestion.suggested_intensity;
}
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<unknown>[] = updates.map(update =>
auditedSupabase.update('questions', update.id, update.data)
);
if (deleteIds.length > 0) {
operations.push(
auditedSupabase.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());
setSelectedIntensityIds(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 toggleIntensitySelection = (id: string) => {
const next = new Set(selectedIntensityIds);
if (next.has(id)) next.delete(id);
else next.add(id);
setSelectedIntensityIds(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 intensitySuggestionCount = suggestions.filter(s => s.textSuggestion?.suggested_intensity != null).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 effectiveIntensityCount = Array.from(selectedIntensityIds).filter(id => !selectedDeleteIds.has(id)).length;
const totalSelectedCount = effectiveTextCount + effectiveTargetCount + effectivePropsCount + effectiveIntensityCount + selectedDeleteIds.size;
const hasSuggestions = textSuggestionCount > 0 || targetSuggestionCount > 0 || propsSuggestionCount > 0 || intensitySuggestionCount > 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 selectAllIntensity = () => setSelectedIntensityIds(new Set(
suggestions.filter(s => s.textSuggestion?.suggested_intensity != null).map(s => s.questionId)
));
const selectNoneIntensity = () => setSelectedIntensityIds(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();
selectAllIntensity();
selectAllDeletes();
};
const selectNone = () => {
selectNoneText();
selectNoneTargets();
selectNoneProps();
selectNoneIntensity();
selectNoneDeletes();
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[95vw] w-full h-[95vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-amber-500" />
Review Questions
</DialogTitle>
<DialogDescription>
AI-powered analysis to improve phrasing, identify props, fix targeting, and suggest deletions.
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto py-4">
{step === 'initial' ? (
<div className="flex flex-col items-center justify-center py-12 text-center space-y-6">
<div className="bg-amber-500/20 p-4 rounded-full">
<Sparkles className="h-12 w-12 text-amber-400" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-medium">Ready to analyze {questions.length} questions</h3>
<p className="text-muted-foreground max-w-lg mx-auto">
The AI will analyze all questions and suggest improvements or removals for:
</p>
</div>
<div className="flex gap-4">
<div className="flex items-center gap-2 bg-purple-500/20 px-4 py-2 rounded-lg">
<Wand2 className="h-5 w-5 text-purple-400" />
<span className="text-sm font-medium text-purple-300">Text Phrasing</span>
</div>
<div className="flex items-center gap-2 bg-indigo-500/20 px-4 py-2 rounded-lg">
<ShieldCheck className="h-5 w-5 text-indigo-400" />
<span className="text-sm font-medium text-indigo-300">Audience Targeting</span>
</div>
<div className="flex items-center gap-2 bg-cyan-500/20 px-4 py-2 rounded-lg">
<Package className="h-5 w-5 text-cyan-400" />
<span className="text-sm font-medium text-cyan-300">Required Props</span>
</div>
<div className="flex items-center gap-2 bg-amber-500/20 px-4 py-2 rounded-lg">
<Gauge className="h-5 w-5 text-amber-400" />
<span className="text-sm font-medium text-amber-300">Intensity Level</span>
</div>
<div className="flex items-center gap-2 bg-rose-500/20 px-4 py-2 rounded-lg">
<Trash2 className="h-5 w-5 text-rose-400" />
<span className="text-sm font-medium text-rose-300">Deletion Suggestions</span>
</div>
</div>
<Button onClick={handleAnalyze} disabled={analyzing} size="lg" className="bg-amber-600 hover:bg-amber-700">
{analyzing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Analyzing...
</>
) : (
'Start Review'
)}
</Button>
</div>
) : (
<div className="space-y-4">
{!hasSuggestions ? (
<div className="text-center py-8 text-muted-foreground">
<Check className="h-8 w-8 mx-auto mb-2 text-green-500" />
All questions look great! No improvements needed.
</div>
) : (
<>
{/* Summary and bulk actions */}
<div className="flex items-center justify-between bg-muted/50 p-3 rounded-lg">
<div className="flex items-center gap-4 text-sm">
<span className="flex items-center gap-1">
<Wand2 className="h-4 w-4 text-purple-500" />
{textSuggestionCount} text fixes
</span>
<span className="flex items-center gap-1">
<ShieldCheck className="h-4 w-4 text-indigo-500" />
{targetSuggestionCount} target fixes
</span>
<span className="flex items-center gap-1">
<Package className="h-4 w-4 text-cyan-500" />
{propsSuggestionCount} props fixes
</span>
<span className="flex items-center gap-1">
<Gauge className="h-4 w-4 text-amber-500" />
{intensitySuggestionCount} intensity fixes
</span>
<span className="flex items-center gap-1">
<Trash2 className="h-4 w-4 text-rose-500" />
{deleteSuggestionCount} delete suggestions
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={selectAll}>
Select All
</Button>
<Button variant="ghost" size="sm" onClick={selectNone}>
Select None
</Button>
</div>
</div>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[18%] min-w-[180px]">Question</TableHead>
<TableHead className="w-[26%]">
<div className="flex items-center gap-2">
<Wand2 className="h-4 w-4 text-purple-500" />
Text Fix
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => selectedTextIds.size === textSuggestionCount ? selectNoneText() : selectAllText()}
>
{selectedTextIds.size === textSuggestionCount ? 'None' : 'All'}
</Button>
</div>
</TableHead>
<TableHead className="w-[18%]">
<div className="flex items-center gap-2">
<ShieldCheck className="h-4 w-4 text-indigo-500" />
Target Fix
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => selectedTargetIds.size === targetSuggestionCount ? selectNoneTargets() : selectAllTargets()}
>
{selectedTargetIds.size === targetSuggestionCount ? 'None' : 'All'}
</Button>
</div>
</TableHead>
<TableHead className="w-[18%]">
<div className="flex items-center gap-2">
<Package className="h-4 w-4 text-cyan-500" />
Props
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => selectedPropsIds.size === propsSuggestionCount ? selectNoneProps() : selectAllProps()}
>
{selectedPropsIds.size === propsSuggestionCount ? 'None' : 'All'}
</Button>
</div>
</TableHead>
<TableHead className="w-[10%]">
<div className="flex items-center gap-2">
<Gauge className="h-4 w-4 text-amber-500" />
Intensity
{intensitySuggestionCount > 0 && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => selectedIntensityIds.size === intensitySuggestionCount ? selectNoneIntensity() : selectAllIntensity()}
>
{selectedIntensityIds.size === intensitySuggestionCount ? 'None' : 'All'}
</Button>
)}
</div>
</TableHead>
<TableHead className="w-[10%]">
<div className="flex items-center gap-2">
<Trash2 className="h-4 w-4 text-rose-500" />
Delete
{deleteSuggestionCount > 0 && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-xs"
onClick={() => selectedDeleteIds.size === deleteSuggestionCount ? selectNoneDeletes() : selectAllDeletes()}
>
{selectedDeleteIds.size === deleteSuggestionCount ? 'None' : 'All'}
</Button>
)}
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{suggestions.map((suggestion) => (
<TableRow key={suggestion.questionId}>
{/* Question Column */}
<TableCell className="align-top py-3">
<div className="text-sm font-medium">{suggestion.question.text}</div>
{suggestion.question.partner_text && (
<div className="text-xs text-muted-foreground mt-1.5 italic">
Partner: {suggestion.question.partner_text}
</div>
)}
</TableCell>
{/* Text Fix Column */}
<TableCell className="align-top py-3">
{suggestion.textSuggestion ? (
<div
className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedTextIds.has(suggestion.questionId)
? 'bg-purple-500/20 border-purple-500/50'
: 'bg-muted/30 border-transparent hover:bg-muted/50'
}`}
onClick={() => toggleTextSelection(suggestion.questionId)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={selectedTextIds.has(suggestion.questionId)}
onCheckedChange={() => toggleTextSelection(suggestion.questionId)}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="text-sm text-purple-300 font-medium">
{suggestion.textSuggestion.suggested_text}
</div>
{suggestion.textSuggestion.suggested_partner_text && (
<div className="text-xs text-purple-400 mt-1.5 italic">
Partner: {suggestion.textSuggestion.suggested_partner_text}
</div>
)}
<div className="text-xs text-muted-foreground mt-2">
{suggestion.textSuggestion.reason}
</div>
</div>
</div>
</div>
) : (
<div className="text-xs text-muted-foreground p-2">
<Check className="h-4 w-4 text-green-500 inline mr-1" />
No changes needed
</div>
)}
</TableCell>
{/* Target Fix Column */}
<TableCell className="align-top py-3">
{suggestion.targetSuggestion ? (
<div
className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedTargetIds.has(suggestion.questionId)
? 'bg-indigo-500/20 border-indigo-500/50'
: 'bg-muted/30 border-transparent hover:bg-muted/50'
}`}
onClick={() => toggleTargetSelection(suggestion.questionId)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={selectedTargetIds.has(suggestion.questionId)}
onCheckedChange={() => toggleTargetSelection(suggestion.questionId)}
className="mt-0.5"
/>
<div className="flex-1 min-w-0 space-y-2">
{/* Couple targets */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Couples:</span>
<Badge variant="outline" className="text-xs text-muted-foreground">
{formatTargets(suggestion.question.allowed_couple_genders)}
</Badge>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge className="text-xs bg-indigo-500/30 text-indigo-300 hover:bg-indigo-500/30">
{formatTargets(suggestion.targetSuggestion.suggested_targets)}
</Badge>
</div>
{/* Initiator */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Initiator:</span>
<Badge variant="outline" className="text-xs text-muted-foreground border-orange-500/50">
{formatInitiator(suggestion.question.target_user_genders)}
</Badge>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge className="text-xs bg-orange-500/30 text-orange-300 hover:bg-orange-500/30">
{formatInitiator(suggestion.targetSuggestion.suggested_initiator)}
</Badge>
</div>
<div className="text-xs text-muted-foreground mt-2">
{suggestion.targetSuggestion.reason}
</div>
</div>
</div>
</div>
) : (
<div className="text-xs text-muted-foreground p-2">
<Check className="h-4 w-4 text-green-500 inline mr-1" />
No changes needed
</div>
)}
</TableCell>
{/* Props Column */}
<TableCell className="align-top py-3">
{suggestion.propsSuggestion ? (
<div
className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedPropsIds.has(suggestion.questionId)
? 'bg-cyan-500/20 border-cyan-500/50'
: 'bg-muted/30 border-transparent hover:bg-muted/50'
}`}
onClick={() => togglePropsSelection(suggestion.questionId)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={selectedPropsIds.has(suggestion.questionId)}
onCheckedChange={() => togglePropsSelection(suggestion.questionId)}
className="mt-0.5"
/>
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Current:</span>
<Badge variant="outline" className="text-xs text-muted-foreground">
{formatProps(suggestion.question.required_props)}
</Badge>
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge className="text-xs bg-cyan-500/30 text-cyan-300 hover:bg-cyan-500/30">
{formatProps(suggestion.propsSuggestion.suggested_required_props)}
</Badge>
</div>
<div className="text-xs text-muted-foreground mt-2">
{suggestion.propsSuggestion.reason}
</div>
</div>
</div>
</div>
) : (
<div className="text-xs text-muted-foreground p-2">
<Check className="h-4 w-4 text-green-500 inline mr-1" />
No changes needed
</div>
)}
</TableCell>
{/* Intensity Column */}
<TableCell className="align-top py-3">
<div className="flex items-center gap-2 mb-1">
<Badge variant="outline" className="text-xs">
Current: {suggestion.question.intensity}
</Badge>
</div>
{suggestion.textSuggestion?.suggested_intensity != null ? (
<div
className={`p-2 rounded-lg border cursor-pointer transition-colors ${selectedIntensityIds.has(suggestion.questionId)
? 'bg-amber-500/20 border-amber-500/50'
: 'bg-muted/30 border-transparent hover:bg-muted/50'
}`}
onClick={() => toggleIntensitySelection(suggestion.questionId)}
>
<div className="flex items-start gap-2">
<Checkbox
checked={selectedIntensityIds.has(suggestion.questionId)}
onCheckedChange={() => toggleIntensitySelection(suggestion.questionId)}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<ArrowRight className="h-3 w-3 text-muted-foreground" />
<Badge className="text-xs bg-amber-500/30 text-amber-300 hover:bg-amber-500/30">
{suggestion.textSuggestion.suggested_intensity}
</Badge>
</div>
{suggestion.textSuggestion.intensity_reason && (
<div className="text-xs text-muted-foreground mt-1">
{suggestion.textSuggestion.intensity_reason}
</div>
)}
</div>
</div>
</div>
) : (
<div className="text-xs text-muted-foreground">
<Check className="h-4 w-4 text-green-500 inline mr-1" />
Correct
</div>
)}
</TableCell>
{/* Delete Column */}
<TableCell className="align-top py-3">
{suggestion.deleteSuggestion ? (
<div
className={`p-3 rounded-lg border cursor-pointer transition-colors ${selectedDeleteIds.has(suggestion.questionId)
? 'bg-rose-500/20 border-rose-500/50'
: 'bg-muted/30 border-transparent hover:bg-muted/50'
}`}
onClick={() => toggleDeleteSelection(suggestion.questionId)}
>
<div className="flex items-start gap-3">
<Checkbox
checked={selectedDeleteIds.has(suggestion.questionId)}
onCheckedChange={() => toggleDeleteSelection(suggestion.questionId)}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="text-sm text-rose-300 font-medium">
{formatDeleteCategory(suggestion.deleteSuggestion.category)} recommended
</div>
{suggestion.deleteSuggestion.duplicate_of_id && (
<div className="text-xs text-rose-400 mt-1">
Duplicate of: {getQuestionTextById(suggestion.deleteSuggestion.duplicate_of_id) || 'Another question'}
</div>
)}
<div className="text-xs text-muted-foreground mt-2">
{suggestion.deleteSuggestion.reason}
</div>
</div>
</div>
</div>
) : (
<div className="text-xs text-muted-foreground p-2">
<Check className="h-4 w-4 text-green-500 inline mr-1" />
Keep
</div>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</>
)}
</div>
)}
</div>
<DialogFooter>
{step === 'review' && (
<>
<Button variant="outline" onClick={() => setStep('initial')}>
Back
</Button>
<Button
onClick={handleApply}
disabled={applying || suggestions.length === 0 || totalSelectedCount === 0}
className="bg-amber-600 hover:bg-amber-700"
>
{applying ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Applying...
</>
) : (
`Apply ${totalSelectedCount} Actions`
)}
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}