feat: added new confidence question in annotation and data modell
This commit is contained in:
@@ -13,9 +13,10 @@ import CalibrationScoresModal from '@/components/CalibrationScoresModal';
|
||||
export interface EntryViewProps {
|
||||
entry: DatasetEntryForAnnotation;
|
||||
index: number;
|
||||
onSave: (rating: number) => Promise<void>;
|
||||
onSave: (rating: number, confidence: number) => Promise<void>;
|
||||
isSaving: boolean;
|
||||
ratingOptions?: { value: number; label: JSX.Element | string }[];
|
||||
confidenceOptions?: { value: number; label: JSX.Element | string }[];
|
||||
question?: JSX.Element | string;
|
||||
}
|
||||
|
||||
@@ -68,6 +69,12 @@ export default function AnnotationPageView({
|
||||
{ value: 4, label: `Klingt eindeutig nach ${DIALECT_LABELS[dialectLabel]}` },
|
||||
],
|
||||
question: <div>Wie authentisch klingt diese Aufnahme nach dem Dialekt der Region <span className="text-blue-600">{DIALECT_LABELS[dialectLabel]}</span>?</div>,
|
||||
confidenceOptions: [
|
||||
{ value: 1, label: 'Sehr unsicher' },
|
||||
{ value: 2, label: 'Eher unsicher' },
|
||||
{ value: 3, label: 'Eher sicher' },
|
||||
{ value: 4, label: 'Sehr sicher' },
|
||||
]
|
||||
};
|
||||
case 'binary':
|
||||
return {
|
||||
@@ -99,6 +106,7 @@ export default function AnnotationPageView({
|
||||
isSaving={isPending}
|
||||
ratingOptions={config.ratingOptions}
|
||||
question={config.question}
|
||||
confidenceOptions={config.confidenceOptions}
|
||||
/>
|
||||
);
|
||||
case 'binary':
|
||||
@@ -147,12 +155,16 @@ export default function AnnotationPageView({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveEntry = async (rating: number) => {
|
||||
const handleSaveEntry = async (rating: number, confidence: number) => {
|
||||
startTransition(async () => {
|
||||
const batch = [{ entryId: currentEntry!.id, rating, dialectLabel: currentEntry!.dialect }];
|
||||
const batch = [{ entryId: currentEntry!.id, rating, dialectLabel: currentEntry!.dialect, confidence }]; // Using max confidence for now, can be changed to user input if needed
|
||||
await saveAnnotations(batch, experimentId);
|
||||
const isNewAnnotation = entries[currentIndex].annotation === null && entries[currentIndex].confidence === null; // Check if this is the first time annotating this entry
|
||||
entries[currentIndex].annotation = rating; // Update local state optimistically
|
||||
handleNext();
|
||||
entries[currentIndex].confidence = confidence; // Update confidence in local state
|
||||
if (isNewAnnotation) {
|
||||
handleNext();
|
||||
}
|
||||
});
|
||||
};
|
||||
// Calculate progress: count already-annotated entries
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function SingleChoiceBinaryEntryView({
|
||||
if (answer === entry.annotation) return; // No change, don't save
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
onSave(answer);
|
||||
onSave(answer, -1); // Using max confidence for now, can be changed to user input if needed
|
||||
}, AUTO_ADVANCE_DELAY_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
|
||||
@@ -7,42 +7,47 @@ import { type EntryViewProps } from './AnnotationPageView';
|
||||
|
||||
const AUTO_ADVANCE_DELAY_MS = 600;
|
||||
|
||||
|
||||
|
||||
export default function SingleChoiceEntryView({
|
||||
entry,
|
||||
index,
|
||||
onSave,
|
||||
isSaving,
|
||||
ratingOptions,
|
||||
confidenceOptions,
|
||||
question,
|
||||
}: EntryViewProps) {
|
||||
const [answer, setAnswer] = useState<number | null>(null);
|
||||
const [answerRating, setAnswerRating] = useState<number | null>(null);
|
||||
const [answerConfidence, setAnswerConfidence] = 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
|
||||
// Auto-save when both answer and confidence are selected
|
||||
useEffect(() => {
|
||||
if (answer === null || isSaving) return;
|
||||
if (answer === entry.annotation) return; // No change, don't save
|
||||
if (answerRating === null || answerConfidence === null || isSaving) return;
|
||||
if (answerRating === entry.annotation && answerConfidence === entry.confidence) return; // No change, don't save
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
onSave(answer);
|
||||
onSave(answerRating, answerConfidence);
|
||||
}, AUTO_ADVANCE_DELAY_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [answer, onSave, entry.annotation, isSaving]);
|
||||
}, [answerRating, answerConfidence, onSave, entry.annotation, entry.confidence, 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
|
||||
setAnswerRating(entry.annotation);
|
||||
setAnswerConfidence(entry.confidence);
|
||||
setFullyPlayed(entry.annotation !== null && entry.confidence !== 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 mt-4 ${
|
||||
fullyPlayed && answer !== null ? 'border-green-300' : 'border-gray-200'
|
||||
fullyPlayed && answerRating !== null && answerConfidence !== null ? 'border-green-300' : 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{/* Sample header */}
|
||||
@@ -79,44 +84,83 @@ export default function SingleChoiceEntryView({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialect + question */}
|
||||
<div className="mt-4 mb-3">
|
||||
<div className="text-s font-semibold text-gray-700">
|
||||
{question}
|
||||
{/* Questions container */}
|
||||
<div className="mt-6 flex flex-col lg:flex-row gap-6">
|
||||
{/* Rating question */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||
{question}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{ratingOptions?.map(({ value, label }) => {
|
||||
const selected = answerRating === value;
|
||||
const disabled = !fullyPlayed || isSaving;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
disabled={disabled}
|
||||
onClick={() => setAnswerRating(value)}
|
||||
className={`flex items-center gap-3 w-full text-left px-4 py-2.5 rounded-lg border text-sm transition-colors ${
|
||||
disabled
|
||||
? 'opacity-40 cursor-not-allowed border-gray-200 bg-gray-50 text-gray-500'
|
||||
: selected
|
||||
? 'border-blue-500 bg-blue-50 text-blue-800 font-medium'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-500 text-white'
|
||||
: 'border-gray-300 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rating buttons */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{ratingOptions?.map(({ value, label }) => {
|
||||
const selected = answer === value;
|
||||
const disabled = !fullyPlayed || isSaving;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
disabled={disabled}
|
||||
onClick={() => setAnswer(value)}
|
||||
className={`flex items-center gap-3 w-full text-left px-4 py-2.5 rounded-lg border text-sm transition-colors ${
|
||||
disabled
|
||||
? 'opacity-40 cursor-not-allowed border-gray-200 bg-gray-50 text-gray-500'
|
||||
: selected
|
||||
? 'border-blue-500 bg-blue-50 text-blue-800 font-medium'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-500 text-white'
|
||||
: 'border-gray-300 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{/* Confidence question */}
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-3">
|
||||
Wie sicher bist du?
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{confidenceOptions?.map(({ value, label }) => {
|
||||
const selected = answerConfidence === value;
|
||||
const disabled = !fullyPlayed || isSaving;
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
disabled={disabled}
|
||||
onClick={() => setAnswerConfidence(value)}
|
||||
className={`flex items-center gap-3 w-full text-left px-4 py-2.5 rounded-lg border text-sm transition-colors ${
|
||||
disabled
|
||||
? 'opacity-40 cursor-not-allowed border-gray-200 bg-gray-50 text-gray-500'
|
||||
: selected
|
||||
? 'border-blue-500 bg-blue-50 text-blue-800 font-medium'
|
||||
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50 text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs font-bold ${
|
||||
selected
|
||||
? 'border-blue-500 bg-blue-500 text-white'
|
||||
: 'border-gray-300 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{selected ? '✓' : ''}
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footnote */}
|
||||
|
||||
Reference in New Issue
Block a user