diff --git a/drizzle/0012_optimal_kabuki.sql b/drizzle/0012_optimal_kabuki.sql new file mode 100644 index 0000000..c845037 --- /dev/null +++ b/drizzle/0012_optimal_kabuki.sql @@ -0,0 +1,2 @@ +ALTER TABLE `experiment` ADD `onboarding_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `experiment` ADD `calibration_enabled` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0012_snapshot.json b/drizzle/meta/0012_snapshot.json new file mode 100644 index 0000000..639cb0a --- /dev/null +++ b/drizzle/meta/0012_snapshot.json @@ -0,0 +1,933 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dbd4c07f-82bc-4016-a950-77d7f86b6d21", + "prevId": "1b5748af-c7c9-49a3-81e7-573dee1f8bff", + "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 + }, + "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 + }, + "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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index e5056ca..2324bc0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1775106689164, "tag": "0011_loving_rumiko_fujikawa", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1775109973706, + "tag": "0012_optimal_kabuki", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/actions/calibration-scoring.ts b/src/app/actions/calibration-scoring.ts index ed41998..4935706 100644 --- a/src/app/actions/calibration-scoring.ts +++ b/src/app/actions/calibration-scoring.ts @@ -2,6 +2,7 @@ import db from '@/lib/db'; import { experiment_calibration } from '@/lib/model/experiment_calibration'; +import { experiment } from '@/lib/model/experiment'; import { participant } from '@/lib/model/participant'; import { eq, and } from 'drizzle-orm'; import { auth } from '@/lib/auth'; @@ -88,6 +89,22 @@ export async function isCalibrationDone(experimentId: number): Promise const session = await auth.api.getSession({ headers: await headers() }); if (!session) throw new Error('Nicht angemeldet'); + // Check if calibration is enabled for this experiment + const exp = await db + .select() + .from(experiment) + .where(eq(experiment.id, experimentId)) + .limit(1); + + if (exp.length === 0) { + throw new Error('Experiment not found'); + } + + // If calibration is not enabled, consider it done + if (!exp[0].calibrationEnabled) { + return true; + } + // Get calibration items for this experiment const calibrationItems = await db .select() diff --git a/src/app/actions/experiment.ts b/src/app/actions/experiment.ts index c5acdcc..fad6903 100644 --- a/src/app/actions/experiment.ts +++ b/src/app/actions/experiment.ts @@ -14,13 +14,17 @@ export async function createExperiment({ description, datasetId, annotationTool, - published = false + published = false, + onboardingEnabled = false, + calibrationEnabled = false }: { name: string; description?: string; datasetId: number; annotationTool?: string; published?: boolean; + onboardingEnabled?: boolean; + calibrationEnabled?: boolean; }) { const result = await requireAdmin(); if (!result.authenticated || !result.admin) { @@ -38,6 +42,8 @@ export async function createExperiment({ datasetId, annotationTool: annotationTool?.trim(), published, + onboardingEnabled, + calibrationEnabled, }); revalidatePath('/admin/experiments'); return result; @@ -49,7 +55,7 @@ export async function createExperiment({ export async function updateExperiment( id: number, - updates: { name?: string; description?: string, annotationTool?: string, published?: boolean } + updates: { name?: string; description?: string, annotationTool?: string, published?: boolean, onboardingEnabled?: boolean, calibrationEnabled?: boolean } ) { const result = await requireAdmin(); if (!result.authenticated || !result.admin) { @@ -74,6 +80,12 @@ export async function updateExperiment( if (updates.published !== undefined) { updateData.published = updates.published; } + if (updates.onboardingEnabled !== undefined) { + updateData.onboardingEnabled = updates.onboardingEnabled; + } + if (updates.calibrationEnabled !== undefined) { + updateData.calibrationEnabled = updates.calibrationEnabled; + } updateData.updatedAt = new Date(); const result = await db diff --git a/src/app/actions/onboarding.ts b/src/app/actions/onboarding.ts index 2aa762e..b6b5bb5 100644 --- a/src/app/actions/onboarding.ts +++ b/src/app/actions/onboarding.ts @@ -2,6 +2,7 @@ import db from '@/lib/db'; import { participant } from '@/lib/model/participant'; +import { experiment } from '@/lib/model/experiment'; import { eq, and } from 'drizzle-orm'; import { auth } from '@/lib/auth'; import { headers } from 'next/headers'; @@ -11,6 +12,22 @@ export async function isOnboardingDone(experimentId: number): Promise { if (!session) throw new Error('Nicht angemeldet'); try { + // Check if onboarding is enabled for this experiment + const exp = await db + .select() + .from(experiment) + .where(eq(experiment.id, experimentId)) + .limit(1); + + if (exp.length === 0) { + throw new Error('Experiment not found'); + } + + // If onboarding is not enabled, consider it done + if (!exp[0].onboardingEnabled) { + return true; + } + const participantRecord = await db .select() .from(participant) diff --git a/src/components/CreateExperimentModal.tsx b/src/components/CreateExperimentModal.tsx index acac876..2257441 100644 --- a/src/components/CreateExperimentModal.tsx +++ b/src/components/CreateExperimentModal.tsx @@ -15,6 +15,8 @@ export default function CreateExperimentModal({ datasetId }: CreateExperimentMod const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [selectedDatasetId, setSelectedDatasetId] = useState(datasetId || ''); + const [onboardingEnabled, setOnboardingEnabled] = useState(false); + const [calibrationEnabled, setCalibrationEnabled] = useState(false); const [loading, setLoading] = useState(false); const [datasetsLoading, setDatasetsLoading] = useState(false); const [error, setError] = useState(null); @@ -61,11 +63,15 @@ export default function CreateExperimentModal({ datasetId }: CreateExperimentMod await createExperiment({ name: name.trim(), description: description.trim() || undefined, - datasetId: Number(selectedDatasetId) + datasetId: Number(selectedDatasetId), + onboardingEnabled, + calibrationEnabled }); setName(''); setDescription(''); setSelectedDatasetId(datasetId || ''); + setOnboardingEnabled(false); + setCalibrationEnabled(false); setSuccess(true); setTimeout(() => { setSuccess(false); @@ -86,6 +92,8 @@ export default function CreateExperimentModal({ datasetId }: CreateExperimentMod setName(''); setDescription(''); setSelectedDatasetId(datasetId || ''); + setOnboardingEnabled(false); + setCalibrationEnabled(false); setError(null); setSuccess(false); } @@ -188,6 +196,34 @@ export default function CreateExperimentModal({ datasetId }: CreateExperimentMod )} +
+ setOnboardingEnabled(e.target.checked)} + className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500" + disabled={loading} + /> + +
+ +
+ setCalibrationEnabled(e.target.checked)} + className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500" + disabled={loading} + /> + +
+ {error && (
{error} diff --git a/src/components/EditableExperimentHeader.tsx b/src/components/EditableExperimentHeader.tsx index 77ddd4f..258107e 100644 --- a/src/components/EditableExperimentHeader.tsx +++ b/src/components/EditableExperimentHeader.tsx @@ -22,6 +22,8 @@ export default function EditableExperimentHeader({ const [description, setDescription] = useState(experiment.description || ''); const [annotationTool, setAnnotationTool] = useState(experiment.annotationTool || 'quality-choice'); const [published, setPublished] = useState(experiment.published || false); + const [onboardingEnabled, setOnboardingEnabled] = useState(experiment.onboardingEnabled || false); + const [calibrationEnabled, setCalibrationEnabled] = useState(experiment.calibrationEnabled || false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -39,7 +41,9 @@ export default function EditableExperimentHeader({ name: name.trim(), description: description.trim() || undefined, annotationTool: annotationTool, - published: published + published: published, + onboardingEnabled: onboardingEnabled, + calibrationEnabled: calibrationEnabled }); setIsEditing(false); } catch (err) { @@ -54,6 +58,8 @@ export default function EditableExperimentHeader({ setAnnotationTool(experiment.annotationTool || 'quality-choice'); setPublished(experiment.published || false); setDescription(experiment.description || ''); + setOnboardingEnabled(experiment.onboardingEnabled || false); + setCalibrationEnabled(experiment.calibrationEnabled || false); setError(null); setIsEditing(false); } @@ -117,6 +123,34 @@ export default function EditableExperimentHeader({
+
+ setOnboardingEnabled(e.target.checked)} + className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500" + disabled={loading} + /> + +
+ +
+ setCalibrationEnabled(e.target.checked)} + className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500" + disabled={loading} + /> + +
+ {error && (
{error} @@ -153,6 +187,9 @@ export default function EditableExperimentHeader({

Status: {experiment.published ? 'Published' : 'Draft'}

+

+ Onboarding: {experiment.onboardingEnabled ? 'Enabled' : 'Disabled'} | Calibration: {experiment.calibrationEnabled ? 'Enabled' : 'Disabled'} +

Created: {new Date(experiment.createdAt).toLocaleDateString()} | Updated: {new Date(experiment.updatedAt).toLocaleDateString()} diff --git a/src/components/OnboardingViews/OnboardingFormView.tsx b/src/components/OnboardingViews/OnboardingFormView.tsx index 22f229c..42dca1e 100644 --- a/src/components/OnboardingViews/OnboardingFormView.tsx +++ b/src/components/OnboardingViews/OnboardingFormView.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useTransition } from 'react'; +import { useEffect, useState, useTransition } from 'react'; import { useRouter } from 'next/navigation'; import { saveOnboardingAnswers } from '@/app/actions/onboarding'; import { DIALECT_LABELS_WITHOUT_DE } from '@/lib/dialects'; @@ -45,6 +45,13 @@ export default function OnboardingFormView({ experimentId }: OnboardingFormViewP }); const [error, setError] = useState(null); + useEffect(() => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }, [currentStep]); + const handleChange = (field: keyof FormData, value: string) => { setFormData((prev) => ({ ...prev, diff --git a/src/components/OnboardingViews/OnboardingPhase.tsx b/src/components/OnboardingViews/OnboardingPhase.tsx index 3b447ec..650a5d5 100644 --- a/src/components/OnboardingViews/OnboardingPhase.tsx +++ b/src/components/OnboardingViews/OnboardingPhase.tsx @@ -5,6 +5,13 @@ import { getOnboardingAnswers } from '@/app/actions/onboarding'; import OnboardingInfoView from './OnboardingInfoView'; import OnboardingFormView from './OnboardingFormView'; +const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); +}; + interface OnboardingPhaseProps { experimentId: number; } @@ -49,14 +56,18 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps) // Show info page first if (showInfoPage) { + return ( setShowInfoPage(false)} + onContinue={() => { + scrollToTop(); + setShowInfoPage(false); + }} hasExistingAnswers={hasExistingAnswers} /> ); } - // Show onboarding form page + return ; } diff --git a/src/lib/model/experiment.ts b/src/lib/model/experiment.ts index 9cf821d..4b1e6e6 100644 --- a/src/lib/model/experiment.ts +++ b/src/lib/model/experiment.ts @@ -11,6 +11,8 @@ export const experiment = sqliteTable( published: integer('published', { mode: 'boolean' }).notNull().default(false), // 0 = draft, 1 = published description: text('description'), annotationTool: text('annotation_tool'), + onboardingEnabled: integer('onboarding_enabled', { mode: 'boolean' }).notNull().default(false), + calibrationEnabled: integer('calibration_enabled', { mode: 'boolean' }).notNull().default(false), datasetId: integer('dataset_id') .notNull() .references(() => dataset.id),