mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
Update
This commit is contained in:
1 parent
a15089f607
commit
456459bb00
41 files changed
+6018
No files matched your search
@@ -0,0 +1,242 @@
|
||||
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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Loader2, ShieldCheck, ArrowRight, Check } from 'lucide-react';
|
||||
import { analyzeQuestionTargets, TargetAnalysis } from '@/lib/openai';
|
||||
import { supabase } from '@/config';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Define local compatible interface
|
||||
interface Question {
|
||||
id: string;
|
||||
text: string;
|
||||
partner_text: string | null;
|
||||
intensity: number;
|
||||
allowed_couple_genders: string[] | null;
|
||||
}
|
||||
|
||||
interface FixTargetsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
questions: Question[];
|
||||
onUpdated: () => void;
|
||||
}
|
||||
|
||||
export function FixTargetsDialog({ open, onOpenChange, questions, onUpdated }: FixTargetsDialogProps) {
|
||||
const [analyzing, setAnalyzing] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<TargetAnalysis[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [step, setStep] = useState<'initial' | 'review'>('initial');
|
||||
const [applying, setApplying] = useState(false);
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
setAnalyzing(true);
|
||||
try {
|
||||
const results = await analyzeQuestionTargets(questions);
|
||||
setSuggestions(results);
|
||||
// Default select all
|
||||
setSelectedIds(new Set(results.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 = suggestions.filter(s => selectedIds.has(s.id));
|
||||
if (updates.length === 0) {
|
||||
onOpenChange(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply updates in parallel
|
||||
await Promise.all(updates.map(update =>
|
||||
supabase
|
||||
.from('questions')
|
||||
.update({ allowed_couple_genders: update.suggested_targets && update.suggested_targets.length > 0 ? update.suggested_targets : null })
|
||||
.eq('id', update.id)
|
||||
));
|
||||
|
||||
toast.success(`Updated ${updates.length} questions`);
|
||||
onUpdated();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Failed to apply updates');
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
// Reset state after transition
|
||||
setTimeout(() => {
|
||||
setStep('initial');
|
||||
setSuggestions([]);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const toggleSelection = (id: string) => {
|
||||
const next = new Set(selectedIds);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
setSelectedIds(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(', ');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-indigo-500" />
|
||||
Fix Question Targets
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use AI to detect anatomical or configuration constraints in your questions.
|
||||
</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-4">
|
||||
<div className="bg-indigo-50 p-4 rounded-full">
|
||||
<ShieldCheck className="h-12 w-12 text-indigo-500" />
|
||||
</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-md mx-auto">
|
||||
The AI will look for anatomical requirements (e.g., acts requiring specific genitalia) and suggest target restrictions.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleAnalyze} disabled={analyzing} size="lg">
|
||||
{analyzing ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Analyzing...
|
||||
</>
|
||||
) : (
|
||||
'Start Analysis'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{suggestions.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Check className="h-8 w-8 mx-auto mb-2 text-green-500" />
|
||||
No issues found! All questions seem to have appropriate targets.
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300 accent-primary"
|
||||
checked={selectedIds.size === suggestions.length && suggestions.length > 0}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) setSelectedIds(new Set(suggestions.map(s => s.id)));
|
||||
else setSelectedIds(new Set());
|
||||
}}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>Question</TableHead>
|
||||
<TableHead>Current</TableHead>
|
||||
<TableHead>Suggested</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{suggestions.map((suggestion) => {
|
||||
const question = questions.find(q => q.id === suggestion.id);
|
||||
if (!question) return null;
|
||||
return (
|
||||
<TableRow key={suggestion.id}>
|
||||
<TableCell>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4 rounded border-gray-300 accent-primary"
|
||||
checked={selectedIds.has(suggestion.id)}
|
||||
onChange={() => toggleSelection(suggestion.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="max-w-xs">
|
||||
<div className="line-clamp-2 text-sm font-medium">{question.text}</div>
|
||||
{question.partner_text && (
|
||||
<div className="text-xs text-muted-foreground mt-1 line-clamp-1">
|
||||
Partner: {question.partner_text}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
<Badge variant="outline" className="text-muted-foreground">
|
||||
{formatTargets(question.allowed_couple_genders)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs whitespace-nowrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
||||
<Badge variant="secondary" className="bg-indigo-100 text-indigo-700 hover:bg-indigo-100">
|
||||
{formatTargets(suggestion.suggested_targets)}
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{suggestion.reason}
|
||||
</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 || selectedIds.size === 0}>
|
||||
{applying ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Applying...
|
||||
</>
|
||||
) : (
|
||||
`Apply ${selectedIds.size} Fixes`
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user