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

@@ -0,0 +1 @@
ALTER TABLE `annotation` ADD `confidence` integer NOT NULL;

View File

@@ -0,0 +1,954 @@
{
"version": "6",
"dialect": "sqlite",
"id": "1413326b-8df5-4c48-9460-9c60528d0393",
"prevId": "a27062e1-cbe8-4669-95fd-2cda4cbaf569",
"tables": {
"annotation": {
"name": "annotation",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"dataset_entry_id": {
"name": "dataset_entry_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"experiment_id": {
"name": "experiment_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"rating": {
"name": "rating",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"confidence": {
"name": "confidence",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dialect_label": {
"name": "dialect_label",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {
"unique_annotation": {
"name": "unique_annotation",
"columns": [
"dataset_entry_id",
"experiment_id",
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"annotation_dataset_entry_id_dataset_entry_id_fk": {
"name": "annotation_dataset_entry_id_dataset_entry_id_fk",
"tableFrom": "annotation",
"tableTo": "dataset_entry",
"columnsFrom": [
"dataset_entry_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"annotation_experiment_id_experiment_id_fk": {
"name": "annotation_experiment_id_experiment_id_fk",
"tableFrom": "annotation",
"tableTo": "experiment",
"columnsFrom": [
"experiment_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"account_id": {
"name": "account_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider_id": {
"name": "provider_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token_expires_at": {
"name": "access_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token_expires_at": {
"name": "refresh_token_expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"account_userId_idx": {
"name": "account_userId_idx",
"columns": [
"user_id"
],
"isUnique": false
}
},
"foreignKeys": {
"account_user_id_user_id_fk": {
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"session": {
"name": "session",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ip_address": {
"name": "ip_address",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_agent": {
"name": "user_agent",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"session_token_unique": {
"name": "session_token_unique",
"columns": [
"token"
],
"isUnique": true
},
"session_userId_idx": {
"name": "session_userId_idx",
"columns": [
"user_id"
],
"isUnique": false
}
},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email_verified": {
"name": "email_verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"admin": {
"name": "admin",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"user_email_unique": {
"name": "user_email_unique",
"columns": [
"email"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"verification": {
"name": "verification",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
}
},
"indexes": {
"verification_identifier_idx": {
"name": "verification_identifier_idx",
"columns": [
"identifier"
],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"dataset_entry": {
"name": "dataset_entry",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"external_id": {
"name": "external_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"speaker_id": {
"name": "speaker_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"model_name": {
"name": "model_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"utterance_id": {
"name": "utterance_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"utterance_text": {
"name": "utterance_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dataset_id": {
"name": "dataset_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"file_name": {
"name": "file_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dialect": {
"name": "dialect",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"iteration": {
"name": "iteration",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"duration_ms": {
"name": "duration_ms",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"rms_value": {
"name": "rms_value",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"longest_pause": {
"name": "longest_pause",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"utmos_score": {
"name": "utmos_score",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"wer_score": {
"name": "wer_score",
"type": "real",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {},
"foreignKeys": {
"dataset_entry_dataset_id_dataset_id_fk": {
"name": "dataset_entry_dataset_id_dataset_id_fk",
"tableFrom": "dataset_entry",
"tableTo": "dataset",
"columnsFrom": [
"dataset_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"dataset": {
"name": "dataset",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"experiment_calibration": {
"name": "experiment_calibration",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"experiment_id": {
"name": "experiment_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"dialect_label": {
"name": "dialect_label",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"utterance_id": {
"name": "utterance_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"utterance_text": {
"name": "utterance_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"order": {
"name": "order",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"file": {
"name": "file",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {},
"foreignKeys": {
"experiment_calibration_experiment_id_experiment_id_fk": {
"name": "experiment_calibration_experiment_id_experiment_id_fk",
"tableFrom": "experiment_calibration",
"tableTo": "experiment",
"columnsFrom": [
"experiment_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"experiment": {
"name": "experiment",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"published": {
"name": "published",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"annotation_tool": {
"name": "annotation_tool",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"onboarding_enabled": {
"name": "onboarding_enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"calibration_enabled": {
"name": "calibration_enabled",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"dataset_id": {
"name": "dataset_id",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {},
"foreignKeys": {
"experiment_dataset_id_dataset_id_fk": {
"name": "experiment_dataset_id_dataset_id_fk",
"tableFrom": "experiment",
"tableTo": "dataset",
"columnsFrom": [
"dataset_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"participant": {
"name": "participant",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"experiment_id": {
"name": "experiment_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"calibration_answers": {
"name": "calibration_answers",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"onboarding_answers": {
"name": "onboarding_answers",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
},
"updated_at": {
"name": "updated_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(unixepoch())"
}
},
"indexes": {
"unique_participant": {
"name": "unique_participant",
"columns": [
"experiment_id",
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"participant_experiment_id_experiment_id_fk": {
"name": "participant_experiment_id_experiment_id_fk",
"tableFrom": "participant",
"tableTo": "experiment",
"columnsFrom": [
"experiment_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -99,6 +99,13 @@
"when": 1775796301988,
"tag": "0013_mute_black_widow",
"breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1775807031115,
"tag": "0014_groovy_patriot",
"breakpoints": true
}
]
}

View File

@@ -156,6 +156,7 @@ export async function getAnnotationEntries(experimentId: number): Promise<Datase
experimentId: experimentId,
datasetId: e.datasetId,
annotation: annotationEntries.find((a) => a.datasetEntryId === e.id)?.rating || null,
confidence: annotationEntries.find((a) => a.datasetEntryId === e.id)?.confidence || null,
utteranceText: e.utteranceText,
}));
@@ -167,7 +168,7 @@ export async function getAnnotationEntries(experimentId: number): Promise<Datase
* Updates existing annotations with the same userId, experimentId, and datasetEntryId.
*/
export async function saveAnnotations(
ratings: { entryId: number; rating: number; dialectLabel: string }[],
ratings: { entryId: number; rating: number; dialectLabel: string; confidence: number }[],
experimentId: number
): Promise<void> {
const session = await auth.api.getSession({ headers: await headers() });
@@ -175,15 +176,16 @@ export async function saveAnnotations(
if (ratings.length === 0) return;
for (const { entryId, rating, dialectLabel } of ratings) {
for (const { entryId, rating, dialectLabel, confidence } of ratings) {
await db
.insert(annotation)
.values({experimentId, datasetEntryId: entryId, userId: session.user.id, rating, dialectLabel })
.values({experimentId, datasetEntryId: entryId, userId: session.user.id, rating, dialectLabel, confidence })
.onConflictDoUpdate({
target: [annotation.userId, annotation.experimentId, annotation.datasetEntryId],
set: {
rating,
dialectLabel,
confidence
}
});
}

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
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,23 +84,22 @@ export default function SingleChoiceEntryView({
</div>
)}
{/* Dialect + question */}
<div className="mt-4 mb-3">
<div className="text-s font-semibold text-gray-700">
{/* 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>
{/* Rating buttons */}
<div className="flex flex-col gap-2">
{ratingOptions?.map(({ value, label }) => {
const selected = answer === value;
const selected = answerRating === value;
const disabled = !fullyPlayed || isSaving;
return (
<button
key={value}
disabled={disabled}
onClick={() => setAnswer(value)}
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'
@@ -118,6 +122,46 @@ export default function SingleChoiceEntryView({
);
})}
</div>
</div>
{/* 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 */}
<p className="text-xs text-gray-400 mt-8 pt-3 border-t border-gray-100">

View File

@@ -28,5 +28,6 @@ export type DatasetEntryForAnnotation = {
datasetId: number;
experimentId: number;
annotation: number | null;
confidence: number | null;
utteranceText: string | null;
};

View File

@@ -11,7 +11,8 @@ export const annotation = sqliteTable(
experimentId: integer('experiment_id')
.references(() => experiment.id),
userId: text('user_id').notNull(),
rating: integer('rating').notNull(), // 15
rating: integer('rating').notNull(), // 14
confidence: integer('confidence').notNull(), // 1-4
dialectLabel: text('dialect_label').notNull(), // e.g. 'ch_be', 'ch_zh', …
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()