Improve onboarding, annotation, and calibration UI

This commit is contained in:
smaubio
2026-04-03 16:03:24 +02:00
parent 7c82befa3d
commit 296fb21b5f
14 changed files with 185 additions and 159 deletions

View File

@@ -54,7 +54,7 @@ export async function getDialectScoresFromCalibration(
if (!answer) continue;
const isCorrect = answer.dialectLabel === item.dialectLabel ? 1 : -1;
const confidence = answer.confidence / 5; // Normalize to 0-1
const confidence = answer.confidence / 4; // Normalize to 0-1
// Score = correctness * confidence
// Correct answers: +confidence (0 to 1)

View File

@@ -47,7 +47,7 @@ export default async function HomePage() {
return (
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-bold text-gray-800 mb-2">Experimente</h1>
<h1 className="text-2xl font-bold text-gray-800 mb-2 mt-4">Experimente</h1>
<p className="text-gray-500 mb-8">
Wählen Sie ein Experiment aus, um mit der Annotation zu beginnen oder fortzufahren.
</p>

View File

@@ -12,6 +12,7 @@ import CalibrationScoresModal from '@/components/CalibrationScoresModal';
export interface EntryViewProps {
entry: DatasetEntryForAnnotation;
index: number;
onSave: (rating: number) => Promise<void>;
isSaving: boolean;
ratingOptions?: { value: number; label: JSX.Element | string }[];
@@ -67,7 +68,7 @@ export default function AnnotationPageView({
{ value: 4, label: `Klingt eher nach ${DIALECT_LABELS[dialectLabel]}` },
{ value: 5, label: `Klingt eindeutig nach ${DIALECT_LABELS[dialectLabel]}` },
],
question: <div>Wie authentisch klingt dieses Sample nach dem Dialekt der Region <span className="text-blue-600">{DIALECT_LABELS[dialectLabel]}</span>?</div>,
question: <div>Wie authentisch klingt diese Aufnahme nach dem Dialekt der Region <span className="text-blue-600">{DIALECT_LABELS[dialectLabel]}</span>?</div>,
};
case 'binary':
return {
@@ -76,7 +77,7 @@ export default function AnnotationPageView({
{ value: 2, label: 'Unklar' },
{ value: 1, label: 'Nein' },
],
question: <div>Klingt dieses Sample nach dem Dialekt der Region <span className="text-blue-600">{DIALECT_LABELS[dialectLabel]}</span>?</div>,
question: <div>Klingt diese Aufnahme nach dem Dialekt der Region <span className="text-blue-600">{DIALECT_LABELS[dialectLabel]}</span>?</div>,
};
default:
return {
@@ -94,6 +95,7 @@ export default function AnnotationPageView({
return (
<SingleChoiceEntryView
entry={currentEntry}
index={currentIndex}
onSave={handleSaveEntry}
isSaving={isPending}
ratingOptions={config.ratingOptions}
@@ -104,6 +106,7 @@ export default function AnnotationPageView({
return (
<SingleChoiceBinaryEntryView
entry={currentEntry}
index={currentIndex}
onSave={handleSaveEntry}
isSaving={isPending}
ratingOptions={config.ratingOptions}
@@ -260,10 +263,10 @@ export default function AnnotationPageView({
{/* Main content area */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Top bar */}
<div className="sticky top-0 z-40 bg-white border-b border-gray-200 pb-4 px-4">
<div className="sticky top-1 z-40 bg-white border-b border-gray-200 pb-5 px-4">
<div className="flex items-center justify-between max-w-4xl mx-auto">
{/* Toggle and Home buttons */}
<div className="flex gap-2">
<div className="flex gap-3">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="hidden md:block text-sm px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md transition-colors"
@@ -287,7 +290,7 @@ export default function AnnotationPageView({
</button>
)}
</div>
<div className="flex gap-2">
<div className="flex gap-3">
<button
onClick={handlePrevious}
disabled={currentIndex === 0 || isPending}

View File

@@ -36,7 +36,7 @@ export default function AnnotationSidebarNavigation({
/>
</div>
<p className="text-xs text-gray-600">
{annotatedCount}/{entries.length} annotiert ({progressPct}%)
{annotatedCount}/{entries.length} bewertet ({progressPct}%)
</p>
</div>
</div>
@@ -47,7 +47,7 @@ export default function AnnotationSidebarNavigation({
{/* Entries List - scrollable middle section with max height */}
<div className="space-y-1 flex-1 min-h-0 overflow-hidden flex flex-col">
<h3 className="text-xs font-semibold text-gray-700 uppercase tracking-wide mb-3 px-2 flex-shrink-0">
Samples
Aufnahmen
</h3>
<div className="space-y-1 overflow-y-auto max-h-150">
{entries.map((entry, index) => (
@@ -77,7 +77,7 @@ export default function AnnotationSidebarNavigation({
/>
{/* Entry number and filename */}
<span className="truncate">
{`Sample ${index + 1} - ${DIALECT_LABELS[entry.dialect]}`}
{`Aufnahme ${index + 1} - ${DIALECT_LABELS[entry.dialect]}`}
</span>
</div>
</button>

View File

@@ -8,6 +8,7 @@ const AUTO_ADVANCE_DELAY_MS = 600;
export default function SingleChoiceBinaryEntryView({
entry,
index,
onSave,
isSaving,
ratingOptions,
@@ -39,14 +40,14 @@ export default function SingleChoiceBinaryEntryView({
return (
<div
className={`border rounded-xl p-5 bg-white shadow-sm transition-colors duration-300 ${
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'
}`}
>
{/* 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}
Aufnahme {index + 1}
</span>
{fullyPlayed && (
<span className="text-xs text-green-600 font-medium">
@@ -64,14 +65,14 @@ export default function SingleChoiceBinaryEntryView({
{/* 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 className="text-xs text-gray-400 mt-2">
Bitte die Aufnahme vollständig anhören, bevor du eine Bewertung abgibst.
</p>
)}
{/* Dialect + question */}
<div className="mt-4 mb-3">
<div className="text-sm font-semibold text-gray-700">
<div className="text-s font-semibold text-gray-700">
{question}
</div>
</div>
@@ -99,6 +100,11 @@ export default function SingleChoiceBinaryEntryView({
);
})}
</div>
{/* Footnote */}
<p className="text-xs text-gray-400 mt-8 pt-3 border-t border-gray-100">
Vertrau deinem ersten Eindruck klingt die Aufnahme authentisch nach diesem Dialekt? Es gibt keine richtige oder falsche Antwort.
</p>
</div>
);
}

View File

@@ -40,14 +40,14 @@ export default function SingleChoiceEntryView({
return (
<div
className={`border rounded-xl p-5 bg-white shadow-sm transition-colors duration-300 ${
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'
}`}
>
{/* 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}
Aufnahme {entry.externalId}
</span>
{fullyPlayed && (
<span className="text-xs text-green-600 font-medium">
@@ -66,7 +66,7 @@ export default function SingleChoiceEntryView({
{/* 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.
Bitte die Aufnahme vollständig anhören, bevor du eine Bewertung abgibst.
</p>
)}

View File

@@ -26,9 +26,7 @@ export default function CalibrationScoresDisplay({
<div key={dialect}>
<div className="flex justify-between items-center mb-2">
<span className="font-medium text-gray-800">{DIALECT_LABELS[dialect]}</span>
<span className={`text-sm font-semibold text-blue-600`}>
{(score * 100).toFixed(0)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
<div
@@ -41,8 +39,7 @@ export default function CalibrationScoresDisplay({
})}
</div>
<p className="text-sm text-gray-500 mt-6">
Die Scores basieren auf der Genauigkeit und dem Vertrauen in Ihre Kalibrierergebnisse.
Dialekte mit höheren Scores werden zuerst in der Annotation angezeigt.
Die Ergebnisse basieren auf deiner Erkennungsgenauigkeit und Sicherheit. Dialekte die du gut erkennst, werden bei der Bewertungsphase häufiger angezeigt.
</p>
</div>
);

View File

@@ -23,7 +23,7 @@ export default function CalibrationScoresModal({
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-lg max-w-2xl w-full mx-4 max-h-96 overflow-y-auto">
<div className="flex justify-between items-center p-6 border-b border-gray-200 sticky top-0 bg-white">
<h2 className="text-xl font-bold">Ihre Kalibrierungsergebnisse</h2>
<h2 className="text-xl font-bold">Deine Kalibrierungsergebnisse</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700"

View File

@@ -16,9 +16,8 @@ interface CalibrationEntryViewProps {
const CONFIDENCE_OPTIONS = [
{ value: 1, label: 'Sehr unsicher' },
{ value: 2, label: 'Eher unsicher' },
{ value: 3, label: 'Neutral' },
{ value: 4, label: 'Eher sicher' },
{ value: 5, label: 'Sehr sicher' },
{ value: 3, label: 'Eher sicher' },
{ value: 4, label: 'Sehr sicher' },
];
export default function CalibrationEntryView({
@@ -70,7 +69,7 @@ export default function CalibrationEntryView({
{/* Sample header */}
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-gray-400 uppercase tracking-wide">
Kalibrierungssample - {entry.order}
Aufnahme {entry.order}
</span>
{fullyPlayed && (
<span className="text-xs text-green-600 font-medium">
@@ -89,7 +88,7 @@ export default function CalibrationEntryView({
{/* 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.
Bitte die Aufnahme vollständig anhören, bevor du eine Bewertung abgibst.
</p>
)}
@@ -98,7 +97,7 @@ export default function CalibrationEntryView({
{/* Dialect question */}
<div className="flex-1">
<div className="text-sm font-semibold text-gray-700 mb-3">
Welcher Dialekt ist es?
Welchen Dialekt hörst du?
</div>
<div className="flex flex-col gap-2">
{Object.entries(DIALECT_LABELS_WITHOUT_DE).map(([dialectKey, dialectName]) => {
@@ -136,7 +135,7 @@ export default function CalibrationEntryView({
{/* Confidence question */}
<div className="flex-1">
<div className="text-sm font-semibold text-gray-700 mb-3">
Wie sicher sind Sie?
Wie sicher bist du?
</div>
<div className="flex flex-col gap-2">
{CONFIDENCE_OPTIONS.map(({ value, label }) => {

View File

@@ -25,21 +25,17 @@ export default function CalibrationInfoView({
return (
<div className="flex flex-col h-full bg-white">
<div className="flex-1 flex items-center justify-center px-4">
<div className="max-w-2xl w-full">
<div className="max-w-[700px] w-full">
{/* Header */}
<div className="text-center mb-4">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Kalibrierungsphase</h1>
<h1 className="text-3xl font-bold text-gray-900 mb-8 mt-4">Kalibrierungsphase</h1>
</div>
{/* Info Section */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-8 mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Was ist die Kalibrierungsphase?</h2>
<p className="text-gray-700 mb-4">
Die Kalibrierungsphase dient dazu, Ihre individuellen Stärken und Schwächen bei der Dialekterkennung zu verstehen. Sie hören verschiedene Audiosamples und geben an, welchen Dialekt Sie hören und wie sicher Sie sich dabei sind.
</p>
<p className="text-gray-700 mb-4">
Dies hilft uns, Ihre Annotationsergebnisse später besser zu bewerten und zu
verstehen. Die Annotationen werden auf Basis Ihrer Kalibrierungsergebnisse gewichtet, sodass Ihre Stärken stärker berücksichtigt werden.
In der Kalibrierungsphase hörst du echte Dialektaufnahmen und gibst an, welchen Dialekt du erkennst und wie sicher du dir dabei bist. So können wir deine Dialektkenntnisse besser einschätzen und deine späteren Bewertungen entsprechend gewichten.
</p>
</div>
@@ -51,38 +47,38 @@ export default function CalibrationInfoView({
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
1
</span>
<span>Sie hören ein Audiosample</span>
<span>Du hörst eine Audioaufnahme</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
2
</span>
<span>Sie wählen den Dialekt aus, den Sie gehört haben</span>
<span>Du wählst den Dialekt aus, den du gehört hast</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
3
</span>
<span>Sie bewerten Ihr Vertrauen in die Identifikation (von 1-5)</span>
<span>Du gibst an, wie sicher du dir bei deiner Wahl bist</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
4
</span>
<span>Sie wiederholen dies für alle Audiosamples</span>
<span>Dies wiederholst du für alle Aufnahmen</span>
</li>
</ol>
</div>
{/* Important Note */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6 mb-8">
<h4 className="font-semibold text-amber-900 mb-2"> Wichtig</h4>
<p className="text-amber-800 text-sm">
Sie können diese Seite jederzeit verlassen und später zurückkehren, um Ihre
Kalibrierung fortzusetzen. Ihre Antworten werden automatisch gespeichert.
{/* Participation Section */}
<div className="mb-8">
<p className="text-gray-700 mb-4">
Die Kalibrierung umfasst 35 Aufnahmen und dauert etwa 510 Minuten.
</p>
</div>
{/* Status */}
{hasExistingAnswers && (
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">

View File

@@ -170,22 +170,21 @@ export default function CalibrationPageView({
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-y-auto">
<div className="max-w-3xl mx-auto px-4">
<div className="text-center mb-12">
<div className="text-5xl mb-4">🎉</div>
<h1 className="text-3xl font-bold text-green-600 mb-3">
<div className="text-center mb-12 mt-4">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
Kalibrierung abgeschlossen!
</h1>
<p className="text-gray-600 mb-8">
Sie haben alle Kalibrierungssamples bewertet. Vielen Dank! Sie können jetzt mit der
Annotation beginnen.
Du hast alle Aufnahmen bewertet. Vielen Dank! Du kannst jetzt mit der Bewertung beginnen.
</p>
</div>
{/* Dialect Scores Summary */}
{sortedScores.length > 0 && (
<div className="mb-12">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Ihre Kalibrierungsergebnisse</h2>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Deine Kalibrierungsergebnisse</h2>
<CalibrationScoresDisplay dialectScores={dialectScores} />
</div>
)}
@@ -194,9 +193,9 @@ export default function CalibrationPageView({
<button
onClick={handleCompleteCalibration}
disabled={isPending}
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors"
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors text-lg"
>
{isPending ? 'Wird gespeichert...' : 'Zur Annotation'}
{isPending ? 'Wird gespeichert...' : 'Zur Bewertung'}
</button>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { DIALECT_LABELS_WITHOUT_DE } from '@/lib/dialects';
interface OnboardingFormViewProps {
experimentId: number;
onBack: () => void;
}
interface ResidenceEntry {
@@ -16,6 +17,7 @@ interface ResidenceEntry {
interface FormData {
age?: string;
email?: string;
residences?: ResidenceEntry[];
dialectQualities?: Record<string, string>;
listeningExperience?: string;
@@ -24,22 +26,18 @@ interface FormData {
type Step = 1 | 2 | 3;
export default function OnboardingFormView({ experimentId }: OnboardingFormViewProps) {
export default function OnboardingFormView({ experimentId, onBack }: OnboardingFormViewProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [currentStep, setCurrentStep] = useState<Step>(1);
// Initialize dialect qualities with default value of '3' for each dialect
const initializeDialectQualities = (): Record<string, string> => {
const qualities: Record<string, string> = {};
Object.keys(DIALECT_LABELS_WITHOUT_DE).forEach(key => {
qualities[key] = '3';
});
return qualities;
return {};
};
const [formData, setFormData] = useState<FormData>({
age: '',
email: '',
residences: [{ region: '', years: '' }],
dialectQualities: initializeDialectQualities()
});
@@ -116,10 +114,10 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
case 3:
const allDialects = Object.keys(DIALECT_LABELS_WITHOUT_DE);
const hasAllDialectQualities = allDialects.every(
(region) => formData.dialectQualities?.[region]
(region) => !!formData.dialectQualities?.[region]
);
if (!hasAllDialectQualities) {
setError('Bitte bewerten Sie Ihre Fähigkeit für alle Dialekte.');
setError('Bitte bewerte deine Fähigkeit für alle Dialekte.');
return false;
}
break;
@@ -138,7 +136,9 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
const handlePrevious = () => {
setError(null);
if (currentStep > 1) {
if (currentStep === 1) {
onBack();
} else {
setCurrentStep((currentStep - 1) as Step);
}
};
@@ -200,33 +200,50 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
{/* Step 1: Persönliche Informationen */}
{currentStep === 1 && (
<div>
<label htmlFor="age" className="block text-sm font-medium text-gray-900 mb-2">
Altersgruppe <span className="text-red-600">*</span>
</label>
<select
id="age"
value={formData.age}
onChange={(e) => handleChange('age', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">-- Bitte wählen --</option>
<option value="18-">Unter 18 Jahre</option>
<option value="18-25">18-25 Jahre</option>
<option value="26-35">26-35 Jahre</option>
<option value="36-45">36-45 Jahre</option>
<option value="46-55">46-55 Jahre</option>
<option value="56-65">56-65 Jahre</option>
<option value="66+">66+ Jahre</option>
</select>
<div className="space-y-6">
<div>
<label htmlFor="age" className="block text-s font-medium text-gray-900 mb-2">
Altersgruppe <span className="text-red-600">*</span>
</label>
<select
id="age"
value={formData.age}
onChange={(e) => handleChange('age', e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">-- Bitte wählen --</option>
<option value="18-">Unter 18 Jahre</option>
<option value="18-25">18-25 Jahre</option>
<option value="26-35">26-35 Jahre</option>
<option value="36-45">36-45 Jahre</option>
<option value="46-55">46-55 Jahre</option>
<option value="56-65">56-65 Jahre</option>
<option value="66+">Über 65 Jahre</option>
</select>
</div>
<div className="border-t border-gray-200 pt-6">
<label htmlFor="email" className="block text-s font-medium text-gray-900 mb-1">
E-Mail-Adresse (optional)
</label>
<p className="text-sm text-gray-500 mb-2">Wird nur für die Gutschein-Verlosung verwendet.</p>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="beispiel@email.ch"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
)}
{/* Step 2: Aufenthaltsorte */}
{currentStep === 2 && (
<div className="space-y-6">
<p className="text-sm text-gray-600 mb-4">
Bitte geben Sie die Regionen an, in denen Sie länger als 1 Jahr gelebt haben, sowie die Anzahl der Jahre. Diese Informationen helfen uns, Ihre Dialektkenntnisse besser einzuschätzen.
<p className="text-s text-gray-600 mb-8">
Bitte gib die Regionen an, in denen du länger als 1 Jahr gelebt hast, sowie die Anzahl der Jahre. So können wir deine Dialektkenntnisse besser einschätzen.
</p>
{/* Map Placeholder */}
<div className="bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg p-12 flex items-center justify-center min-h-[300px]">
@@ -238,8 +255,8 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
{/* Residence Entries */}
<div>
<label className="block text-sm font-medium text-gray-900 mb-4">
Aufenthaltsorte, die länger als 1 Jahr waren <span className="text-red-600">*</span>
<label className="block text-s font-medium text-gray-900 mb-4">
Aufenthaltsorte (mind. 1 Jahr) <span className="text-red-600">*</span>
</label>
<div className="space-y-4">
@@ -310,62 +327,71 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
{/* Step 3: Selbsteinschätzung */}
{currentStep === 3 && (
<div className="space-y-6">
<div>
<p className="text-sm text-gray-600 mb-6">
Bewerten Sie Ihre Fähigkeit, die folgenden Dialekte zu identifizieren, auf einer Skala von "sehr schlecht" bis "sehr gut". Diese Selbsteinschätzung hilft uns, Ihre nachfolgendenKalibrierungsergebnisse besser zu interpretieren.
</p>
<p className="text-s text-gray-600 mb-8">
Bewerte deine Fähigkeit, die folgenden Dialekte zu erkennen. Diese Selbsteinschätzung hilft uns, deine Kalibrierungsergebnisse besser zu interpretieren.
</p>
<div className="space-y-5">
{Object.entries(DIALECT_LABELS_WITHOUT_DE).map(([key, label]) => {
const qualityLabels: Record<string, string> = {
'1': 'Sehr schlecht',
'2': 'Schlecht',
'3': 'Weder noch',
'4': 'Gut',
'5': 'Sehr gut',
};
const currentValue = formData.dialectQualities?.[key] || '3';
<ul className="text-s text-gray-600 mb-8 space-y-1">
<li><strong>1</strong> = Sehr schlecht</li>
<li><strong>2</strong> = Schlecht</li>
<li><strong>3</strong> = Gut</li>
<li><strong>4</strong> = Sehr gut</li>
<li><strong>N</strong> = Nicht vertraut</li>
</ul>
<table className="w-full border-collapse">
<thead>
<tr>
<th className="text-sm font-medium text-gray-700 text-left pb-2"></th>
<th className="w-[120px] text-xs font-medium text-gray-700 text-center pb-2">1</th>
<th className="w-[120px] text-xs font-medium text-gray-700 text-center pb-2">2</th>
<th className="w-[120px] text-xs font-medium text-gray-700 text-center pb-2">3</th>
<th className="w-[120px] text-xs font-medium text-gray-700 text-center pb-2">4</th>
<th className="w-[120px] text-xs font-medium text-gray-700 text-center pb-2 border-l border-gray-200">N</th>
</tr>
</thead>
<tbody>
{Object.entries(DIALECT_LABELS_WITHOUT_DE).map(([key, label], index) => {
const currentValue = formData.dialectQualities?.[key] ?? '';
return (
<div key={key}>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-900">
{label}
</label>
<span className="text-sm font-semibold text-blue-600 bg-blue-50 px-3 py-1 rounded-full">
{qualityLabels[currentValue]}
</span>
</div>
<div>
<tr key={key} className={index % 2 === 0 ? 'bg-gray-50' : ''}>
<td className="text-sm font-medium text-gray-900 py-3">{label}</td>
{['1', '2', '3', '4'].map((value) => (
<td key={value} className="w-[120px] text-center py-3">
<input
type="radio"
name={key}
value={value}
checked={currentValue === value}
onChange={() => handleDialectQualityChange(key, value)}
className="cursor-pointer accent-blue-600 w-5 h-5"
/>
</td>
))}
<td className="w-[120px] text-center py-3 border-l border-gray-200">
<input
type="range"
min="1"
max="5"
step="1"
value={currentValue}
onChange={(e) =>
handleDialectQualityChange(key, e.target.value)
}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
type="radio"
name={key}
value="-1"
checked={currentValue === '-1'}
onChange={() => handleDialectQualityChange(key, '-1')}
className="cursor-pointer accent-blue-600 w-5 h-5"
/>
</div>
<div className="flex justify-between text-xs text-gray-600 mt-2 font-medium">
<span>Sehr schlecht</span>
<span>Sehr gut</span>
</div>
</div>
</td>
</tr>
);
})}
</div>
</div>
</tbody>
</table>
</div>
)}
{/* Navigation Buttons */}
<div className="flex gap-4 justify-between pt-8">
<div className="flex gap-4 justify-between pt-8 mb-8">
<button
type="button"
onClick={handlePrevious}
disabled={currentStep === 1 || isPending}
disabled={isPending}
className="px-6 py-3 bg-gray-300 hover:bg-gray-400 disabled:bg-gray-200 text-gray-800 font-semibold rounded-lg transition-colors"
>
Zurück
@@ -387,18 +413,13 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP
disabled={isPending}
className="px-8 py-3 bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white font-semibold rounded-lg transition-colors"
>
{isPending ? 'Wird gespeichert...' : 'Abschließen'}
{isPending ? 'Wird gespeichert...' : 'Abschliessen'}
</button>
)}
</div>
</form>
{/* Info */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mt-8">
<p className="text-blue-800 text-sm">
Die mit <span className="text-red-600">*</span> gekennzeichneten Felder sind erforderlich.
</p>
</div>
</div>
</div>
</div>

View File

@@ -16,54 +16,59 @@ export default function OnboardingInfoView({
return (
<div className="flex flex-col h-full bg-white">
<div className="flex-1 flex items-center justify-center px-4">
<div className="max-w-2xl w-full">
<div className="max-w-[700px] w-full">
{/* Header */}
<div className="text-center mb-4">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Willkommen</h1>
<div className="text-center mb-8 mt-4">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Willkommen und vielen Dank für deine Teilnahme!</h1>
</div>
{/* Info Section */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-8 mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Was ist das Ziel?</h2>
<h2 className="text-xl font-semibold text-gray-900 mb-4">Worum geht es?</h2>
<p className="text-gray-700 mb-4">
Vielen Dank, dass Sie an diesem Experiment teilnehmen. Bevor Sie mit der Annotation beginnen, möchten wir gerne ein paar grundlegende Informationen über Sie und Ihre Erfahrung mit Dialekten sammeln.
Im Rahmen unserer Bachelorarbeit an der ZHAW untersuchen wir, wie authentisch ein KI-Modell Schweizerdeutsche Dialekte synthetisieren kann. Dazu sind wir auf deine Einschätzung angewiesen.
</p>
<p className="text-gray-700 mb-4">
Diese Informationen helfen uns, Ihre Annotationsergebnisse besser zu verstehen und zu bewerten. Alle Angaben werden vertraulich behandelt.
Deine Aufgabe ist es, kurze Audioaufnahmen anzuhören und zu bewerten, wie authentisch sie nach einem bestimmten Dialekt klingen. Es gibt dabei keine richtigen oder falschen Antworten wichtig ist deine persönliche Einschätzung.
</p>
</div>
{/* Instructions Section */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Wie funktioniert es?</h3>
<h3 className="text-lg font-semibold text-gray-900 mb-4">So läuft die Teilnahme ab:</h3>
<ol className="space-y-3 text-gray-700">
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
1
</span>
<span>Sie beantworten ein kurzes Umfrage-Formular</span>
<span>Du beantwortest zuerst ein paar kurze Fragen zu dir und deiner Erfahrung mit Schweizer Dialekten</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
2
</span>
<span>Die Antworten werden gespeichert</span>
<span>Danach folgt eine kurze Kalibrierungsphase mit echten Dialektaufnahmen</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
3
</span>
<span>Danach geht es zur Kalibrierungsphase</span>
</li>
<li className="flex gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold">
4
</span>
<span>Anschließend können Sie mit der Annotation beginnen</span>
<span>Anschliessend beginnt die eigentliche Bewertung der synthetischen Aufnahmen</span>
</li>
</ol>
</div>
{/* Participation Section */}
<div className="mb-8">
<p className="text-gray-700 mb-4">
Du entscheidest selbst, wie viele Aufnahmen du bewertest. Die gesamte Teilnahme inklusive Formular, Kalibrierung und 50 bewerteten Aufnahmen dauert etwa 30 Minuten. Unter allen Teilnehmenden, die 50 oder mehr Aufnahmen bewertet haben, <strong>verlosen wir 3x [Platzhalter]-Gutscheine im Wert von je [x].- CHF!</strong>
</p>
<p className="text-gray-700 mb-4">
Alle Angaben werden anonym ausgewertet und ausschliesslich zu Forschungszwecken verwendet.
</p>
</div>
{/* Status */}
{hasExistingAnswers && (
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">
@@ -77,9 +82,9 @@ export default function OnboardingInfoView({
<div className="flex justify-center">
<button
onClick={handleContinue}
className="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
className="px-8 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors text-lg"
>
Weiter zum Formular
Jetzt starten
</button>
</div>
</div>

View File

@@ -69,5 +69,5 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
}
return <OnboardingFormView experimentId={experimentId} />;
return <OnboardingFormView experimentId={experimentId} onBack={() => { scrollToTop(); setShowInfoPage(true); }} />;
}