"use client"; import {useState, useCallback, ChangeEvent, useEffect, useRef} from 'react'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card'; import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import {Slider} from '@/components/ui/slider'; import {useToast} from '@/hooks/use-toast'; import {Toaster} from '@/components/ui/toaster'; import {Icons} from '@/components/icons'; import {Separator} from '@/components/ui/separator'; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuItem, SidebarProvider, } from '@/components/ui/sidebar'; import {Progress} from '@/components/ui/progress'; import { exportToCsv, getAllRatings, saveRating, saveUserInfo, } from '@/services/rating-service'; import {UserInfo} from '@/ai/db/schema'; import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table'; import {fetchAudioFiles} from '@/services/audio-service'; import {fetchDriveAudioFiles} from '@/services/drive-service'; import Image from 'next/image'; // Define the type based on the return value of getAllRatings() type RatingEntry = { userId: string; userInfo?: { name: string; email: string; }; audioA: string; audioB: string; rating: { audioA: string; audioB: string; ratingA: number; ratingB: number; }; }; const PasswordPage = () => { const [password, setPassword] = useState(''); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isDeveloper, setIsDeveloper] = useState(false); const [loginError, setLoginError] = useState(null); const {toast} = useToast(); // Minimal config state for admin info only const [config, setConfig] = useState<{ adminName?: string; adminEmail?: string; }>({}); const passwordInputRef = useRef(null); useEffect(() => { // Focus on the password input when the component mounts passwordInputRef.current?.focus(); }, []); // Update handlePasswordSubmit to authenticate via API const handlePasswordSubmit = async () => { try { setLoginError(null); const response = await fetch('/api/config', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password, name, email, }), }); const data = await response.json(); if (response.ok && data.success) { setIsAuthenticated(true); setIsDeveloper(data.isDeveloper); if (data.isDeveloper) { // Store admin info if user is developer setConfig({ adminName: data.adminName, adminEmail: data.adminEmail, }); toast({ title: 'Developer login successful!', description: `Welcome, ${name}!`, }); } else { // Regular user login const userInfo: UserInfo = {name, email}; await saveUserInfo(email, userInfo); // Use email as userId toast({ title: 'Login successful!', description: `Welcome, ${name}!`, }); } } else { setIsAuthenticated(false); setIsDeveloper(false); setLoginError(data.error || 'Invalid credentials'); toast({ title: 'Login failed', description: data.error || 'Incorrect credentials.', }); } } catch (error: any) { console.error('Authentication error:', error); setLoginError('Authentication failed. Please try again.'); toast({ title: 'Login failed', description: 'Authentication failed. Please try again.', }); } }; const handleEnterKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handlePasswordSubmit(); } }; if (!isAuthenticated) { return (
Hi! Paris Logo Enter Credentials
setName(e.target.value)} />
setEmail(e.target.value)} />
setPassword(e.target.value)} onKeyDown={handleEnterKeyPress} ref={passwordInputRef} />
{loginError &&

{loginError}

} {/* Demo Credentials */}

Demo Credentials

Regular User:
Name: Any name
Email: Any email
Password: demo
Admin User:
Name: admin
Email: admin@tts.com
Password: admin
💡 Admin users can export results and see additional analytics
); } return ; }; const AudioRaterApp = ({ isDeveloper, userEmail, userName, config, // Add config to props }: { isDeveloper: boolean; userEmail: string; userName: string; config: { // Define the config type adminName?: string; adminEmail?: string; }; }) => { const [audioPairs, setAudioPairs] = useState<[string, string, string, boolean][]>([]); const [currentPairIndex, setCurrentPairIndex] = useState(0); const [ratings, setRatings] = useState<{ a: number; b: number }[]>([]); const {toast} = useToast(); const [ratedPairs, setRatedPairs] = useState([]); const [tempRatings, setTempRatings] = useState<{ a: number; b: number }>({ a: 0, b: 0, }); // Temporary ratings before submission const [ratingSubmitted, setRatingSubmitted] = useState(false); const [allRatings, setAllRatings] = useState([]); const [loading, setLoading] = useState(true); const [audioLoading, setAudioLoading] = useState(true); const [audioError, setAudioError] = useState(null); const [testCompleted, setTestCompleted] = useState(false); // State to track test completion useEffect(() => { const loadAudioFiles = async () => { setAudioLoading(true); setAudioError(null); // Clear any previous errors try { // Only use local files - remove Google Drive fetch const localFiles = await fetchAudioFiles(); if (localFiles && localFiles.length > 0) { setAudioPairs(localFiles); } else { setAudioError('No audio files found.'); toast({ title: 'No audio files found', description: 'Please check the audio files.' }); } } catch (error) { console.error('Error loading audio files:', error); setAudioError('Failed to load audio files.'); toast({ title: 'Error loading audio files', description: 'Failed to load audio files. Please try again.' }); } finally { setAudioLoading(false); } }; loadAudioFiles(); }, [toast]); useEffect(() => { loadInitialRatings(); }, [userEmail, audioPairs]); const loadInitialRatings = async () => { setLoading(true); try { const loadedRatings = await getAllRatings(); setAllRatings(loadedRatings); // Initialize ratings state with existing ratings for the current user const initialRatings = audioPairs.map(([audioA, audioB, audioName]) => { const existingRating = loadedRatings.find( rating => rating.userId === userEmail && rating.audioA === audioA && rating.audioB === audioB )?.rating; return existingRating ? {a: existingRating.ratingA, b: existingRating.ratingB} : {a: 0, b: 0}; }); setRatings(initialRatings); setRatedPairs( initialRatings.reduce((acc: number[], rating, index) => { if (rating.a > 0 && rating.b > 0) { acc.push(index); } return acc; }, []) ); // Set initial temporary ratings if there are existing ratings if (initialRatings[currentPairIndex]) { setTempRatings({ a: initialRatings[currentPairIndex].a || 0, b: initialRatings[currentPairIndex].b || 0, }); } } catch (error) { console.error('Error loading ratings:', error); toast({ title: 'Error loading ratings', description: 'Failed to load existing ratings. Please try again.', }); } finally { setLoading(false); } }; const handleRatingChange = (version: 'a' | 'b', rating: number) => { setTempRatings({...tempRatings, [version]: rating}); }; const handleSubmitRating = async () => { try { const {a, b} = tempRatings; if (a < 1 || a > 5 || b < 1 || b > 5) { toast({ title: 'Invalid rating', description: 'Please rate both versions between 1 and 5.', }); return; } const [audioA, audioB, audioName, swapped] = audioPairs[currentPairIndex]; // When saving to database, ensure the original file order is maintained await saveRating( userEmail, swapped ? audioB : audioA, // If swapped, audioB is the improved version swapped ? audioA : audioB, // If swapped, audioA is the raw version { ratingA: swapped ? tempRatings.b : tempRatings.a, // Map rating to correct audio ratingB: swapped ? tempRatings.a : tempRatings.b // Map rating to correct audio } ); const newRatings = [...ratings]; newRatings[currentPairIndex] = {a: tempRatings.a, b: tempRatings.b}; setRatings(newRatings); if (!ratedPairs.includes(currentPairIndex)) { setRatedPairs([...ratedPairs, currentPairIndex]); } toast({ title: 'Rating saved!', description: 'Your rating has been successfully saved.', }); setRatingSubmitted(true); // Move to the next pair automatically const nextIndex = (currentPairIndex + 1) % audioPairs.length; handlePairSelect(nextIndex); } catch (error: any) { console.error('Error submitting rating:', error.message); toast({ title: 'Error saving rating', description: 'Failed to save your rating. Please try again.', }); } }; const handlePairSelect = (index: number) => { setCurrentPairIndex(index); setRatingSubmitted(false); setTempRatings({ a: ratings[index]?.a || 0, b: ratings[index]?.b || 0, }); }; const ratingPercentage = audioPairs.length > 0 ? (ratedPairs.length / audioPairs.length) * 100 : 0; const currentPair = audioPairs[currentPairIndex]; const handleExportCsv = async () => { try { const csvData = await exportToCsv(); if (!csvData) { toast({title: 'No data to export', description: 'No ratings found.'}); return; } const blob = new Blob([csvData], {type: 'text/csv'}); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.setAttribute('href', url); a.setAttribute('download', 'audio_ratings.csv'); document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); toast({title: 'CSV Exported', description: 'Data exported successfully.'}); } catch (error: any) { console.error('CSV Export Error:', error); toast({title: 'Export Failed', description: 'Error exporting data.'}); } }; const handleFinishTest = async () => { if (ratedPairs.length === audioPairs.length) { setTestCompleted(true); // Send export email to admin try { const adminEmail = config.adminEmail || 'tim.horstmann@ip-paris.fr'; const response = await fetch('/api/send-export', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ adminEmail: adminEmail, }), }); if (response.ok) { console.log('Export email sent successfully'); } else { console.error('Failed to send export email'); } } catch (error) { console.error('Error sending export email:', error); } } else { toast({ title: 'Cannot finish A/B test', description: 'Please rate all audio pairs before finishing.', }); } }; if (isDeveloper) { return (

Developer View - All Ratings

{loading ? (
Loading...
) : ( A list of all audio ratings in the database. User ID User Name User Email Audio A Audio B Rating A Rating B {allRatings.map((rating, index) => ( {rating.userId} {rating.userInfo?.name} {rating.userInfo?.email} {rating.audioA} {rating.audioB} {rating.rating.ratingA} {rating.rating.ratingB} ))}
)}
); } if (testCompleted) { return (
Thank You!

Thank you for your participation in this A/B testing!

); } return ( Audio Pairs {audioPairs.map((pair, index) => ( ))}
{/* Header with progress */}

Audio A/B Test

Hi! Paris Logo
Your progress {ratedPairs.length}/{audioPairs.length} pairs rated
{currentPair && (

Currently rating: {currentPair[2]}

)}
{audioLoading ? (

Loading audio files...

) : audioError ? (

Error: {audioError}

) : currentPair ? (
Audio Pair {currentPairIndex + 1} of {audioPairs.length} {currentPair[2]}
{/* Enhanced Audio Players - Stacked for more space */}
handleRatingChange('a', rating)} /> handleRatingChange('b', rating)} />
{ratingSubmitted ? (
Rating submitted
) : ( "Please rate both versions to continue" )}
{currentPairIndex > 0 && ( )} {ratingSubmitted && ( )}
) : (

No audio pairs available.

)} {/* Finish test button - repositioned */}

Ready to complete the test?

After finishing, your ratings will be submitted for analysis.

); }; const EnhancedAudioPlayer = ({ src, title, rating, onRatingChange }: { src: string; title: string; rating: number; onRatingChange: (rating: number) => void; }) => { // For Google Drive URLs, use the full URL directly, for local files normalize the path const audioSrc = src.startsWith('https://') ? src : src.replace(/\\/g, '/'); return (

{title === "Version A" ? "A" : "B"} {title}

Score: {rating}
Poor onRatingChange(value[0])} className="flex-1" /> Excellent
); }; const AudioPlayer = ({src, title}: { src: string; title: string }) => { // For Google Drive URLs, use the full URL directly, for local files normalize the path const audioSrc = src.startsWith('https://') ? src : src.replace(/\\/g, '/'); return (
); }; const RatingSelector = ({ version, onChange, currentRating, }: { version: 'a' | 'b'; onChange: (version: 'a' | 'b', rating: number) => void; currentRating: number; }) => { return (
onChange(version, value[0])} className="max-w-xs" /> {currentRating}
); }; export default PasswordPage;