import React, { useEffect, useRef } from 'react'; import { Scale, Fingerprint, Play, X } from 'lucide-react'; import { toast } from 'react-hot-toast'; import { PRESETS } from '../utils/constants'; import { generateSpeech } from '../api/generate'; import { Button, Panel, Field, Textarea, Select } from '../ui'; import './CompareModal.css'; export default function CompareModal({ open, onClose, profiles, compareText, setCompareText, compareVoiceA, setCompareVoiceA, compareVoiceB, setCompareVoiceB, compareResultA, setCompareResultA, compareResultB, setCompareResultB, compareProgress, setCompareProgress, isComparing, setIsComparing, steps, cfg, speed, denoise, postprocess, fileToMediaUrl, loadHistory, }) { const drawerRef = useRef(null); useEffect(() => { if (!open) return; const onKey = (e) => { if (e.key === 'Escape') onClose?.(); }; const onPointer = (e) => { if (drawerRef.current && !drawerRef.current.contains(e.target)) onClose?.(); }; window.addEventListener('keydown', onKey); // Defer click-outside so the opening click doesn't immediately close. const t = setTimeout(() => window.addEventListener('mousedown', onPointer), 0); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('mousedown', onPointer); clearTimeout(t); }; }, [open, onClose]); const runCompare = async () => { setIsComparing(true); setCompareResultA(null); setCompareResultB(null); const generateVoice = async (voiceId) => { setCompareProgress('Preparing voice...'); const formData = new FormData(); formData.append('text', compareText); let fin_prof = voiceId; let fin_inst = ''; if (fin_prof.startsWith('preset:')) { const pr = PRESETS.find(p => p.id === fin_prof.replace('preset:', '')); if (pr) { const parts = Object.values(pr.attrs).filter(v => v !== 'Auto'); fin_inst = parts.join(', '); } fin_prof = ''; } else if (profiles.find(p => p.id === fin_prof)?.instruct) { fin_inst = profiles.find(p => p.id === fin_prof).instruct; } if (fin_prof) formData.append('profile_id', fin_prof); if (fin_inst) formData.append('instruct', fin_inst); formData.append('num_step', steps); formData.append('guidance_scale', cfg); formData.append('speed', speed); formData.append('denoise', denoise); formData.append('postprocess_output', postprocess); const res = await generateSpeech(formData); const blob = await res.blob(); const urls = await fileToMediaUrl(blob, null); return urls.audioUrl; }; try { setCompareProgress('Generating Voice A...'); const audioA = await generateVoice(compareVoiceA); setCompareResultA(audioA); setCompareProgress('Generating Voice B...'); const audioB = await generateVoice(compareVoiceB); setCompareResultB(audioB); setCompareProgress(''); toast.success('Comparison complete!'); loadHistory(); } catch (err) { toast.error('Play failed: ' + err.message); setCompareProgress(''); } finally { setIsComparing(false); } }; const canCompare = !isComparing && compareVoiceA && compareVoiceB && compareText.trim(); if (!open) return null; return (

Compare two voices side by side to make casting decisions. App stays interactive behind.