feat: added new confidence question in annotation and data modell

This commit is contained in:
averel10
2026-04-10 10:11:18 +02:00
parent 88e8287224
commit f6bf277f88
9 changed files with 1076 additions and 54 deletions

View File

@@ -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

View File

@@ -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);

View File

@@ -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 */}