105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import WaveformPlayer from '../WaveformPlayer';
|
|
import { type EntryViewProps } from './AnnotationPageView';
|
|
|
|
const AUTO_ADVANCE_DELAY_MS = 600;
|
|
|
|
export default function SingleChoiceBinaryEntryView({
|
|
entry,
|
|
onSave,
|
|
isSaving,
|
|
ratingOptions,
|
|
question,
|
|
}: EntryViewProps) {
|
|
const [answer, setAnswer] = useState<number | null>(null);
|
|
const [fullyPlayed, setFullyPlayed] = useState<boolean>(false);
|
|
|
|
const fileExt = entry.fileName.substring(entry.fileName.lastIndexOf('.'));
|
|
const audioSrc = `/public/datasets/${entry.datasetId}/${entry.externalId}${fileExt}`;
|
|
|
|
// Auto-save when answer is selected
|
|
useEffect(() => {
|
|
if (answer === null || isSaving) return;
|
|
if (answer === entry.annotation) return; // No change, don't save
|
|
|
|
const timer = setTimeout(() => {
|
|
onSave(answer);
|
|
}, AUTO_ADVANCE_DELAY_MS);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [answer, onSave, entry.annotation, isSaving]);
|
|
|
|
useEffect(() => {
|
|
// Reset state when entry changes
|
|
setAnswer(entry.annotation);
|
|
setFullyPlayed(entry.annotation !== null); // If already annotated, mark as fully played to allow changing answer
|
|
}, [entry]);
|
|
|
|
return (
|
|
<div
|
|
className={`border rounded-xl p-5 bg-white shadow-sm transition-colors duration-300 ${
|
|
fullyPlayed && answer !== null ? 'border-green-300' : 'border-gray-200'
|
|
}`}
|
|
>
|
|
{/* Sample header */}
|
|
<div className="flex items-center justify-between mb-3">
|
|
<span className="text-xs font-medium text-gray-400 uppercase tracking-wide">
|
|
Sample - {entry.externalId}
|
|
</span>
|
|
{fullyPlayed && (
|
|
<span className="text-xs text-green-600 font-medium">
|
|
✓ Gehört
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Waveform player */}
|
|
<WaveformPlayer
|
|
src={audioSrc}
|
|
durationMs={entry.durationMs}
|
|
onFullyPlayed={() => setFullyPlayed(true)}
|
|
/>
|
|
|
|
{/* Must-listen hint */}
|
|
{!fullyPlayed && (
|
|
<p className="text-xs text-amber-600 mt-2">
|
|
Bitte das Sample vollständig anhören, bevor Sie eine Bewertung abgeben.
|
|
</p>
|
|
)}
|
|
|
|
{/* Dialect + question */}
|
|
<div className="mt-4 mb-3">
|
|
<p className="text-sm font-semibold text-gray-700">
|
|
{question}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Radio Select */}
|
|
<div className="flex items-center gap-2 bg-gray-50 p-1.5 rounded-full border border-gray-200 w-fit mt-4">
|
|
{ratingOptions?.map(({ value, label }) => {
|
|
const selected = answer === value;
|
|
const disabled = !fullyPlayed || isSaving;
|
|
return (
|
|
<button
|
|
key={value}
|
|
disabled={disabled}
|
|
onClick={() => setAnswer(value)}
|
|
className={`px-6 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
|
|
disabled
|
|
? 'opacity-50 cursor-not-allowed text-gray-400'
|
|
: selected
|
|
? 'bg-white text-blue-600 shadow-sm border border-blue-200'
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
>
|
|
{label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|