implement calibration scoring and display features
This commit is contained in:
100
src/components/CalibrationViews/CalibrationInfoView.tsx
Normal file
100
src/components/CalibrationViews/CalibrationInfoView.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client';
|
||||
|
||||
interface CalibrationInfoViewProps {
|
||||
onContinue: () => void;
|
||||
hasExistingAnswers: boolean;
|
||||
}
|
||||
|
||||
export default function CalibrationInfoView({
|
||||
onContinue,
|
||||
hasExistingAnswers,
|
||||
}: CalibrationInfoViewProps) {
|
||||
const handleContinue = () => {
|
||||
onContinue();
|
||||
};
|
||||
|
||||
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">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12 mt-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions Section */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Wie funktioniert es?</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 hören ein Audiosample</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>
|
||||
</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>
|
||||
</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>
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
{hasExistingAnswers && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-6 mb-8">
|
||||
<p className="text-green-800 text-sm">
|
||||
✓ Sie haben diese Einführung bereits gesehen. Sie können Ihre Kalibrierung
|
||||
fortsetzen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
<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 text-lg"
|
||||
>
|
||||
{hasExistingAnswers ? 'Kalibrierung fortsetzen' : 'Kalibrierung starten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,11 @@
|
||||
import { useState, useTransition, useMemo, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { saveCalibrationAnswers, getCalibrationAnswers } from '@/app/actions/calibration';
|
||||
import { getDialectScoresFromCalibration } from '@/app/actions/calibration-scoring';
|
||||
import { ExperimentCalibration } from '@/lib/model/experiment_calibration';
|
||||
import CalibrationEntryView from './CalibrationEntryView';
|
||||
import CalibrationScoresDisplay from '@/components/CalibrationScoresDisplay';
|
||||
import { DIALECT_LABELS } from '@/lib/dialects';
|
||||
|
||||
interface CalibrationPageViewProps {
|
||||
entries: ExperimentCalibration[];
|
||||
@@ -27,6 +30,7 @@ export default function CalibrationPageView({
|
||||
const [answers, setAnswers] = useState<CalibrationAnswers>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [dialectScores, setDialectScores] = useState<Record<string, number>>({});
|
||||
|
||||
// Load existing answers on component mount
|
||||
useEffect(() => {
|
||||
@@ -64,6 +68,21 @@ export default function CalibrationPageView({
|
||||
);
|
||||
const isComplete = entries.length > 0 && completeAnswers.length === entries.length;
|
||||
const progressPct = Math.round((completeAnswers.length / entries.length) * 100);
|
||||
|
||||
// Load dialect scores when calibration is complete
|
||||
useEffect(() => {
|
||||
if (isComplete && !isLoading) {
|
||||
const loadScores = async () => {
|
||||
try {
|
||||
const scores = await getDialectScoresFromCalibration(experimentId);
|
||||
setDialectScores(scores);
|
||||
} catch (error) {
|
||||
console.error('Error loading dialect scores:', error);
|
||||
}
|
||||
};
|
||||
loadScores();
|
||||
}
|
||||
}, [isComplete, isLoading, experimentId]);
|
||||
|
||||
// Check if current entry has complete answers
|
||||
const currentEntryComplete = currentEntry &&
|
||||
@@ -142,40 +161,44 @@ export default function CalibrationPageView({
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-4">⏳</div>
|
||||
<p className="text-gray-600">Laden Sie Ihre vorherigen Antworten...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
// Sort dialect scores from highest to lowest
|
||||
const sortedScores = Object.entries(dialectScores).sort((a, b) => b[1] - a[1]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="max-w-3xl mx-auto text-center py-20">
|
||||
<div className="text-5xl mb-4">🎉</div>
|
||||
<h1 className="text-3xl font-bold text-green-600 mb-3">
|
||||
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.
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
{isPending ? 'Wird gespeichert...' : 'Zur Annotation'}
|
||||
</button>
|
||||
<div className="max-w-3xl mx-auto py-20 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">
|
||||
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.
|
||||
</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>
|
||||
|
||||
<CalibrationScoresDisplay dialectScores={dialectScores} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<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"
|
||||
>
|
||||
{isPending ? 'Wird gespeichert...' : 'Zur Annotation'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getCalibrationEntries } from '@/app/actions/calibration';
|
||||
import { getCalibrationEntries, getCalibrationAnswers } from '@/app/actions/calibration';
|
||||
import { ExperimentCalibration } from '@/lib/model/experiment_calibration';
|
||||
import CalibrationPageView from './CalibrationPageView';
|
||||
import CalibrationInfoView from './CalibrationInfoView';
|
||||
|
||||
interface CalibrationPhaseProps {
|
||||
experimentId: number;
|
||||
}
|
||||
|
||||
export default function CalibrationPhase({ experimentId }: CalibrationPhaseProps) {
|
||||
const [entries, setEntries] = useState<ExperimentCalibration[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [entries, setEntries] = useState<ExperimentCalibration[]>([]);
|
||||
const [showInfoPage, setShowInfoPage] = useState(true);
|
||||
const [hasExistingAnswers, setHasExistingAnswers] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const result = await getCalibrationEntries(experimentId);
|
||||
if (result.success && result.data) {
|
||||
// Load calibration entries
|
||||
const entriesResult = await getCalibrationEntries(experimentId);
|
||||
if (entriesResult.success && entriesResult.data) {
|
||||
// Sort by order
|
||||
const sorted = [...result.data].sort((a, b) => a.order - b.order);
|
||||
const sorted = [...entriesResult.data].sort((a, b) => a.order - b.order);
|
||||
setEntries(sorted);
|
||||
}
|
||||
|
||||
// Load existing answers to check if user has seen the info page before
|
||||
const answersResult = await getCalibrationAnswers(experimentId);
|
||||
if (answersResult.success && answersResult.data) {
|
||||
const answers = answersResult.data as Record<number, any> | null;
|
||||
if (answers && Object.keys(answers).length > 0) {
|
||||
setHasExistingAnswers(true);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Fehler beim Laden der Kalibrierungssamples');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -34,27 +45,28 @@ export default function CalibrationPhase({ experimentId }: CalibrationPhaseProps
|
||||
load();
|
||||
}, [experimentId]);
|
||||
|
||||
// Show loading view
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-16 text-center">
|
||||
<div className="text-5xl mb-4">⏳</div>
|
||||
<h1 className="text-2xl font-bold mb-4">Lädt Kalibrierungssamples...</h1>
|
||||
<p className="text-gray-600">Bitte warten Sie...</p>
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Loading calibration...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !entries || entries.length === 0) {
|
||||
// Show info page first
|
||||
if (showInfoPage) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto py-16 text-center">
|
||||
<div className="text-5xl mb-4">❌</div>
|
||||
<h1 className="text-2xl font-bold mb-4">Kalibrierung nicht verfügbar</h1>
|
||||
<p className="text-gray-600 mb-8">
|
||||
{error || 'Für dieses Experiment sind keine Kalibrierungssamples vorhanden.'}
|
||||
</p>
|
||||
</div>
|
||||
<CalibrationInfoView
|
||||
onContinue={() => setShowInfoPage(false)}
|
||||
hasExistingAnswers={hasExistingAnswers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show calibration page
|
||||
return <CalibrationPageView entries={entries} experimentId={experimentId} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user