diff --git a/src/app/annotate/[experimentId]/page.tsx b/src/app/annotate/[experimentId]/page.tsx
index d088c22..c3b3c9c 100644
--- a/src/app/annotate/[experimentId]/page.tsx
+++ b/src/app/annotate/[experimentId]/page.tsx
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getAnnotationEntries } from '@/app/actions/annotations';
import { getExperimentById } from '@/app/actions/experiment';
-import QualityChoiceView from '@/components/AnnotationViews/QualityChoiceView';
+import AnnotationPageView from '@/components/AnnotationViews/AnnotationPageView';
interface Props {
params: Promise<{ experimentId: string}>;
@@ -52,25 +52,11 @@ export default async function AnnotatePage({ params }: Props) {
);
}
- // Render based on prototype type
- const renderAnnotationView = () => {
- switch (prototype) {
- case 'quality-choice':
- return ;
- case 'binary':
- //return ;
- return
;
- default:
- return (
-
-
Unbekannter Annotation-Typ: {prototype}
-
- ← Startseite
-
-
- );
- }
- };
-
- return renderAnnotationView();
+ return (
+
+ );
}
diff --git a/src/components/AnnotationViews/QualityChoiceView.tsx b/src/components/AnnotationViews/AnnotationPageView.tsx
similarity index 57%
rename from src/components/AnnotationViews/QualityChoiceView.tsx
rename to src/components/AnnotationViews/AnnotationPageView.tsx
index 6eb7939..17c4eb2 100644
--- a/src/components/AnnotationViews/QualityChoiceView.tsx
+++ b/src/components/AnnotationViews/AnnotationPageView.tsx
@@ -1,20 +1,93 @@
'use client';
-import { useState, useMemo, useTransition } from 'react';
+import { useState, useMemo, useTransition, JSX } from 'react';
import Link from 'next/link';
import { saveAnnotations } from '@/app/actions/annotations';
-import { type DatasetEntryForAnnotation } from '@/lib/dialects';
-import QualityChoiceEntryView from './QualityChoiceEntryView';
+import { DIALECT_LABELS, type DatasetEntryForAnnotation } from '@/lib/dialects';
+import SingleChoiceEntryView from './SingleChoiceEntryView';
+import SingleChoiceBinaryEntryView from './SingleChoiceBinaryEntryView';
-interface QualityChoiceViewProps {
- entries: DatasetEntryForAnnotation[];
- experimentId: number;
+export interface EntryViewProps {
+ entry: DatasetEntryForAnnotation;
+ onSave: (rating: number) => Promise;
+ isSaving: boolean;
+ ratingOptions?: { value: number; label: JSX.Element | string }[];
+ question?: JSX.Element | string;
}
-export default function QualityChoicePage({ entries, experimentId }: QualityChoiceViewProps) {
+interface AnnotationPageViewProps {
+ entries: DatasetEntryForAnnotation[];
+ experimentId: number;
+ viewType: string;
+}
+
+export default function AnnotationPageView({
+ entries,
+ experimentId,
+ viewType,
+}: AnnotationPageViewProps) {
const [isPending, startTransition] = useTransition();
+ const getAnnotationConfig = (dialectLabel: string) => {
+ switch (viewType) {
+ case 'quality-choice':
+ return {
+ ratingOptions: [
+ { value: 1, label: `Klingt überhaupt nicht nach ${DIALECT_LABELS[dialectLabel]}` },
+ { value: 2, label: `Klingt eher nicht nach ${DIALECT_LABELS[dialectLabel]}` },
+ { value: 3, label: 'Schwer zu beurteilen' },
+ { value: 4, label: `Klingt eher nach ${DIALECT_LABELS[dialectLabel]}` },
+ { value: 5, label: `Klingt eindeutig nach ${DIALECT_LABELS[dialectLabel]}` },
+ ],
+ question: Wie authentisch klingt dieses Sample nach dem Dialekt der Region {DIALECT_LABELS[dialectLabel]}?
,
+ };
+ case 'binary':
+ return {
+ ratingOptions: [
+ { value: 0, label: 'Ja' },
+ { value: 2, label: 'Unklar' },
+ { value: 1, label: 'Nein' },
+ ],
+ question: Klingt dieses Sample nach dem Dialekt der Region {DIALECT_LABELS[dialectLabel]}?
,
+ };
+ default:
+ return {
+ ratingOptions: [{ value: 1, label: '' }],
+ question: ,
+ };
+ }
+ };
+
+ // Determine which view component to use based on viewType
+ const getViewComponent = () => {
+ const config = getAnnotationConfig(currentEntry.dialect);
+ switch (viewType) {
+ case 'quality-choice':
+ return (
+
+ );
+ case 'binary':
+ return (
+
+ );
+ default:
+ return null;
+ }
+ };
+
// Find the first unannotated entry to start from
const startingIndex = useMemo(() => {
const firstUnannotatedIndex = entries.findIndex((e) => e.annotation === null);
@@ -46,11 +119,23 @@ export default function QualityChoicePage({ entries, experimentId }: QualityChoi
handleNext();
});
};
-
// Calculate progress: count already-annotated entries
const annotatedCount = entries.filter((e) => e.annotation !== null).length;
const progressPct = Math.round(((annotatedCount) / entries.length) * 100);
+ // Check if view type is valid
+ const viewElement = getViewComponent();
+ if (!viewElement) {
+ return (
+
+
Unbekannter Annotation-Typ: {viewType}
+
+ ← Startseite
+
+
+ );
+ }
+
if (isComplete) {
return (
@@ -113,13 +198,7 @@ export default function QualityChoicePage({ entries, experimentId }: QualityChoi
{/* ── Sample entry ────────────────────────────────────── */}
- {currentEntry && (
-
- )}
+ {viewElement}
);
diff --git a/src/components/AnnotationViews/SingleChoiceBinaryEntryView.tsx b/src/components/AnnotationViews/SingleChoiceBinaryEntryView.tsx
new file mode 100644
index 0000000..9345bce
--- /dev/null
+++ b/src/components/AnnotationViews/SingleChoiceBinaryEntryView.tsx
@@ -0,0 +1,104 @@
+'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(null);
+ const [fullyPlayed, setFullyPlayed] = useState(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 (
+
+ {/* Sample header */}
+
+
+ Sample - {entry.externalId}
+
+ {fullyPlayed && (
+
+ ✓ Gehört
+
+ )}
+
+
+ {/* Waveform player */}
+
setFullyPlayed(true)}
+ />
+
+ {/* Must-listen hint */}
+ {!fullyPlayed && (
+
+ Bitte das Sample vollständig anhören, bevor Sie eine Bewertung abgeben.
+
+ )}
+
+ {/* Dialect + question */}
+
+
+ {/* Radio Select */}
+
+ {ratingOptions?.map(({ value, label }) => {
+ const selected = answer === value;
+ const disabled = !fullyPlayed || isSaving;
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/components/AnnotationViews/QualityChoiceEntryView.tsx b/src/components/AnnotationViews/SingleChoiceEntryView.tsx
similarity index 80%
rename from src/components/AnnotationViews/QualityChoiceEntryView.tsx
rename to src/components/AnnotationViews/SingleChoiceEntryView.tsx
index 6006109..cd3d18d 100644
--- a/src/components/AnnotationViews/QualityChoiceEntryView.tsx
+++ b/src/components/AnnotationViews/SingleChoiceEntryView.tsx
@@ -3,32 +3,20 @@
import { useState, useEffect } from 'react';
import WaveformPlayer from '../WaveformPlayer';
import { type DatasetEntryForAnnotation, DIALECT_LABELS } from '@/lib/dialects';
+import { type EntryViewProps } from './AnnotationPageView';
const AUTO_ADVANCE_DELAY_MS = 600;
-const RATING_OPTIONS = (dialectLabel: string) => [
- { value: 1, label: `Klingt überhaupt nicht nach ${dialectLabel}` },
- { value: 2, label: `Klingt eher nicht nach ${dialectLabel}` },
- { value: 3, label: 'Schwer zu beurteilen' },
- { value: 4, label: `Klingt eher nach ${dialectLabel}` },
- { value: 5, label: `Klingt eindeutig nach ${dialectLabel}` },
-];
-
-interface QualityChoiceEntryViewProps {
- entry: DatasetEntryForAnnotation;
- onSave: (rating: number) => Promise;
- isSaving: boolean;
-}
-
-export default function QualityChoiceEntryView({
+export default function SingleChoiceEntryView({
entry,
onSave,
isSaving,
-}: QualityChoiceEntryViewProps) {
+ ratingOptions,
+ question,
+}: EntryViewProps) {
const [answer, setAnswer] = useState(null);
const [fullyPlayed, setFullyPlayed] = useState(false);
- const dialectLabel = DIALECT_LABELS[entry.dialect] ?? entry.dialect;
const fileExt = entry.fileName.substring(entry.fileName.lastIndexOf('.'));
const audioSrc = `/public/datasets/${entry.datasetId}/${entry.externalId}${fileExt}`;
@@ -85,14 +73,13 @@ export default function QualityChoiceEntryView({
{/* Dialect + question */}
- Wie authentisch klingt dieses Sample nach dem Dialekt der Region{' '}
- {dialectLabel}?
+ {question}
{/* Rating buttons */}
- {RATING_OPTIONS(dialectLabel).map(({ value, label }) => {
+ {ratingOptions?.map(({ value, label }) => {
const selected = answer === value;
const disabled = !fullyPlayed || isSaving;
return (