diff --git a/drizzle/0010_luxuriant_jigsaw.sql b/drizzle/0010_luxuriant_jigsaw.sql new file mode 100644 index 0000000..ae51d95 --- /dev/null +++ b/drizzle/0010_luxuriant_jigsaw.sql @@ -0,0 +1,2 @@ +ALTER TABLE `experiment` ADD `published` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `experiment` ADD `annotation_tool` text; \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..a24528c --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,748 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "e31e8601-8dc5-451c-ad3d-d4038b8e63ea", + "prevId": "4773999c-10cb-4a54-b2e8-aec0988f7a7b", + "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": true, + "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": {}, + "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": { + "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 + }, + "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": {} + } + }, + "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 a543343..72334a8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -71,6 +71,13 @@ "when": 1773985687853, "tag": "0009_mysterious_brother_voodoo", "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1773987751513, + "tag": "0010_luxuriant_jigsaw", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/actions/annotations.ts b/src/app/actions/annotations.ts index 27d0d6f..bfe89e6 100644 --- a/src/app/actions/annotations.ts +++ b/src/app/actions/annotations.ts @@ -87,7 +87,7 @@ export async function saveAnnotations( /** Returns all datasets (for the home page). */ export async function getAllExperiments() { - return db.select().from(experiment).orderBy(experiment.id); + return db.select().from(experiment).where(eq(experiment.published, true)).orderBy(experiment.id); } /** Returns annotation progress for the current user in a dataset. */ diff --git a/src/app/actions/datasets.ts b/src/app/actions/datasets.ts index 8297c42..79b532e 100644 --- a/src/app/actions/datasets.ts +++ b/src/app/actions/datasets.ts @@ -7,6 +7,24 @@ import { revalidatePath } from 'next/cache'; import { removeAllDatasetEntries } from './remove-dataset-entries'; import { requireAdmin } from '@/lib/auth'; +export async function getAllDatasets() { + const result = await requireAdmin(); + if (!result.authenticated || !result.admin) { + throw new Error('Unauthorized'); + } + + try { + const datasets = await db + .select() + .from(dataset) + .orderBy(dataset.name); + return datasets; + } catch (error) { + console.error('Error fetching datasets:', error); + throw new Error('Failed to fetch datasets'); + } +} + export async function createDataset({ name, description }: { name: string; description?: string }) { const result = await requireAdmin(); if (!result.authenticated || !result.admin) { diff --git a/src/app/actions/experiment.ts b/src/app/actions/experiment.ts index e69de29..46d407b 100644 --- a/src/app/actions/experiment.ts +++ b/src/app/actions/experiment.ts @@ -0,0 +1,146 @@ +'use server'; + +import db from '@/lib/db'; +import { experiment } from '@/lib/model/experiment'; +import { and, eq } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; +import { auth, requireAdmin } from '@/lib/auth'; +import { headers } from 'next/headers'; + +export async function createExperiment({ + name, + description, + datasetId, + annotationTool, + published = false +}: { + name: string; + description?: string; + datasetId: number; + annotationTool: string; + published?: boolean; +}) { + const result = await requireAdmin(); + if (!result.authenticated || !result.admin) { + throw new Error('Unauthorized'); + } + + if (!name || !name.trim()) { + throw new Error('Experiment name is required'); + } + + try { + const result = await db.insert(experiment).values({ + name: name.trim(), + description: description?.trim() || null, + datasetId, + annotationTool: annotationTool.trim(), + published, + }); + revalidatePath('/admin/experiments'); + return result; + } catch (error) { + console.error('Error creating experiment:', error); + throw new Error('Failed to create experiment'); + } +} + +export async function updateExperiment( + id: number, + updates: { name?: string; description?: string, annotationTool?: string, published?: boolean } +) { + const result = await requireAdmin(); + if (!result.authenticated || !result.admin) { + throw new Error('Unauthorized'); + } + + if (updates.name !== undefined && !updates.name.trim()) { + throw new Error('Experiment name cannot be empty'); + } + + try { + const updateData: any = {}; + if (updates.name !== undefined) { + updateData.name = updates.name.trim(); + } + if (updates.description !== undefined) { + updateData.description = updates.description.trim() || null; + } + if (updates.annotationTool !== undefined) { + updateData.annotationTool = updates.annotationTool; + } + if (updates.published !== undefined) { + updateData.published = updates.published; + } + updateData.updatedAt = new Date(); + + const result = await db + .update(experiment) + .set(updateData) + .where(eq(experiment.id, id)); + revalidatePath(`/admin/experiments/${id}`); + revalidatePath('/admin/experiments'); + return result; + } catch (error) { + console.error('Error updating experiment:', error); + throw new Error('Failed to update experiment'); + } +} + +export async function deleteExperiment(id: number) { + const result = await requireAdmin(); + if (!result.authenticated || !result.admin) { + throw new Error('Unauthorized'); + } + + try { + // Delete the experiment + await db.delete(experiment).where(eq(experiment.id, id)); + + revalidatePath('/admin/experiments'); + return { success: true }; + } catch (error) { + console.error('Error deleting experiment:', error); + throw new Error('Failed to delete experiment'); + } +} + +export async function getExperimentsByDataset(datasetId: number) { + const result = await requireAdmin(); + if (!result.authenticated || !result.admin) { + throw new Error('Unauthorized'); + } + + try { + const experiments = await db + .select() + .from(experiment) + .where(eq(experiment.datasetId, datasetId)); + return experiments; + } catch (error) { + console.error('Error fetching experiments:', error); + throw new Error('Failed to fetch experiments'); + } +} + +export async function getExperimentById(id: number) { + const session = await auth.api.getSession({ headers: await headers() }); + if (!session) throw new Error('Nicht angemeldet'); + + + try { const experimentData = await db + .select() + .from(experiment) + .where( + and( + eq(experiment.id, id), + eq(experiment.published, true) + )) + .limit(1); + return experimentData[0] || null; + } + catch (error) { + console.error('Error fetching experiment:', error); + throw new Error('Failed to fetch experiment'); + } +} diff --git a/src/app/admin/experiments/[id]/page.tsx b/src/app/admin/experiments/[id]/page.tsx new file mode 100644 index 0000000..3f8e294 --- /dev/null +++ b/src/app/admin/experiments/[id]/page.tsx @@ -0,0 +1,100 @@ +import Link from 'next/link'; +import db from '@/lib/db'; +import { experiment } from '@/lib/model/experiment'; +import { eq } from 'drizzle-orm'; +import EditableExperimentHeader from '@/components/EditableExperimentHeader'; +import DeleteExperimentButton from '@/components/DeleteExperimentButton'; +import { requireAdmin } from '@/lib/auth'; +import { redirect } from 'next/navigation'; + +interface ExperimentPageProps { + params: { + id: string; + }; +} + +export default async function ExperimentPage({ params }: ExperimentPageProps) { + const result = await requireAdmin(); + + if (!result.authenticated) { + redirect("/user/sign-in"); + } + + if (!result.admin) { + redirect("/"); + } + + const { id } = await params; + const experimentId = parseInt(id, 10); + + const experiments = await db + .select() + .from(experiment) + .where(eq(experiment.id, experimentId)); + + if (experiments.length === 0) { + return ( +
Experiment not found
+Experiment ID
+{exp.id}
+Dataset ID
+{exp.datasetId}
+Created
+{new Date(exp.createdAt).toLocaleString()}
+Last Updated
+{new Date(exp.updatedAt).toLocaleString()}
+Annotation Tool
+{exp.annotationTool}
+Manage all experiments across datasets
+Manage experiments globally or go to the experiments management page
+- Wählen Sie einen Annotationsprototyp aus, um mit der Bewertung zu beginnen. -
- -{proto.description}
-Unbekannter Annotation-Typ: {prototype}
+ + ← Startseite + ++ This will permanently delete the experiment. This action cannot be undone. +
+ + {error && ( +{experiment.description}
++ ID: {experiment.id} | Dataset ID: {experiment.datasetId} +
++ Annotation Tool: {ANNOTATION_TOOLS.find(t => t.value === (experiment.annotationTool || 'single-choice'))?.label || 'Single Choice'} +
++ Status: {experiment.published ? 'Published' : 'Draft'} +
++ Created: {new Date(experiment.createdAt).toLocaleDateString()} | + Updated: {new Date(experiment.updatedAt).toLocaleDateString()} +
+ +No experiments found.
+ ) : ( +{exp.description}
++ ID: {exp.id} | Dataset ID: {exp.datasetId} +
++ Created: {new Date(exp.createdAt).toLocaleDateString()} +
+ + ))} +