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 ( +
+
+ + ← Back to Experiments + +
+

Experiment not found

+
+
+
+ ); + } + + const exp = experiments[0]; + + return ( +
+
+ + ← Back to Experiments + + + + +
+ +
+ +
+

Experiment Details

+
+
+

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}

+
+
+
+
+
+ ); +} diff --git a/src/app/admin/experiments/page.tsx b/src/app/admin/experiments/page.tsx new file mode 100644 index 0000000..db576ee --- /dev/null +++ b/src/app/admin/experiments/page.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link'; +import ExperimentsList from '@/components/ExperimentsList'; +import CreateExperimentModal from '@/components/CreateExperimentModal'; +import { requireAdmin } from '@/lib/auth'; +import { redirect } from 'next/navigation'; + +export default async function ExperimentsAdminPage() { + const result = await requireAdmin(); + + if (!result.authenticated) { + redirect("/user/sign-in"); + } + + if (!result.admin) { + redirect("/"); + } + + return ( +
+ + ← Back to Admin + + +
+

Experiments Management

+

Manage all experiments across datasets

+
+ + + + +
+ ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1623152..f66239f 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,5 +1,6 @@ import DatasetsList from "@/components/DatasetsList"; import CreateDatasetModal from "@/components/CreateDatasetModal"; +import ExperimentsList from "@/components/ExperimentsList"; import { AdminTokenForm } from "@/components/AdminTokenForm"; import BuildInfo from "@/components/BuildInfo"; import { requireAdmin } from "@/lib/auth"; @@ -28,6 +29,12 @@ export default async function AdminPage() { > Manage Users + + Manage Experiments + @@ -40,6 +47,12 @@ export default async function AdminPage() { + +
+

Experiments

+

Manage experiments globally or go to the experiments management page

+ +
); } diff --git a/src/app/annotate/[datasetId]/page.tsx b/src/app/annotate/[datasetId]/page.tsx deleted file mode 100644 index ff66baf..0000000 --- a/src/app/annotate/[datasetId]/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { redirect } from 'next/navigation'; -import { headers } from 'next/headers'; -import Link from 'next/link'; -import { auth } from '@/lib/auth'; - -const PROTOTYPES = [ - { - id: 'single-choice', - name: 'Single Choice', - description: 'Bewerten Sie jedes Sample auf einer Skala von 1–5.', - }, -]; - -interface Props { - params: Promise<{ datasetId: string }>; -} - -export default async function PrototypeSelectionPage({ params }: Props) { - const session = await auth.api.getSession({ headers: await headers() }); - if (!session) redirect('/user/sign-in'); - - const { datasetId } = await params; - - return ( -
- - ← Zurück zur Startseite - -

Prototyp auswählen

-

- Wählen Sie einen Annotationsprototyp aus, um mit der Bewertung zu beginnen. -

- -
- {PROTOTYPES.map((proto) => ( - -
-
-

{proto.name}

-

{proto.description}

-
- -
- - ))} -
-
- ); -} diff --git a/src/app/annotate/[datasetId]/[prototype]/page.tsx b/src/app/annotate/[experimentId]/page.tsx similarity index 63% rename from src/app/annotate/[datasetId]/[prototype]/page.tsx rename to src/app/annotate/[experimentId]/page.tsx index 199dde9..7c67175 100644 --- a/src/app/annotate/[datasetId]/[prototype]/page.tsx +++ b/src/app/annotate/[experimentId]/page.tsx @@ -4,17 +4,19 @@ import Link from 'next/link'; import { auth } from '@/lib/auth'; import { getAnnotationEntries } from '@/app/actions/annotations'; import SingleChoiceView from '@/components/AnnotationViews/SingleChoiceView'; +import { getExperimentById } from '@/app/actions/experiment'; interface Props { - params: Promise<{ experimentId: string; prototype: string }>; + params: Promise<{ experimentId: string}>; } export default async function AnnotatePage({ params }: Props) { const session = await auth.api.getSession({ headers: await headers() }); if (!session) redirect('/user/sign-in'); - const { experimentId: experimentIdStr, prototype } = await params; + const { experimentId: experimentIdStr } = await params; const experimentId = parseInt(experimentIdStr, 10); + const prototype = await getExperimentById(experimentId).then(exp => exp?.annotationTool || null); if (isNaN(experimentId)) { return ( @@ -27,9 +29,6 @@ export default async function AnnotatePage({ params }: Props) { ); } - if (prototype !== 'single-choice') { - notFound(); - } const entries = await getAnnotationEntries(experimentId); @@ -53,5 +52,23 @@ export default async function AnnotatePage({ params }: Props) { ); } - return ; + // Render based on prototype type + const renderAnnotationView = () => { + switch (prototype) { + case 'single-choice': + return ; + // Add more prototypes here as needed + default: + return ( +
+

Unbekannter Annotation-Typ: {prototype}

+ + ← Startseite + +
+ ); + } + }; + + return renderAnnotationView(); } diff --git a/src/components/CreateExperimentModal.tsx b/src/components/CreateExperimentModal.tsx new file mode 100644 index 0000000..acac876 --- /dev/null +++ b/src/components/CreateExperimentModal.tsx @@ -0,0 +1,206 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { createExperiment } from '@/app/actions/experiment'; +import Modal from '@/components/Modal'; +import { Dataset } from '@/lib/model/dataset'; +import { getAllDatasets } from '@/app/actions/datasets'; + +interface CreateExperimentModalProps { + datasetId?: number; +} + +export default function CreateExperimentModal({ datasetId }: CreateExperimentModalProps) { + const [isOpen, setIsOpen] = useState(false); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [selectedDatasetId, setSelectedDatasetId] = useState(datasetId || ''); + const [loading, setLoading] = useState(false); + const [datasetsLoading, setDatasetsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [datasets, setDatasets] = useState([]); + + useEffect(() => { + if (isOpen && !datasetId) { + // Fetch datasets if not pre-selected + fetchDatasets(); + } + }, [isOpen, datasetId]); + + async function fetchDatasets() { + try { + setDatasetsLoading(true); + const data = await getAllDatasets(); + setDatasets(data); + } catch (err) { + console.error('Failed to fetch datasets:', err); + setError('Failed to load datasets'); + } finally { + setDatasetsLoading(false); + } + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setSuccess(false); + + if (!name.trim()) { + setError('Experiment name is required'); + return; + } + + if (!selectedDatasetId) { + setError('Dataset is required'); + return; + } + + setLoading(true); + try { + await createExperiment({ + name: name.trim(), + description: description.trim() || undefined, + datasetId: Number(selectedDatasetId) + }); + setName(''); + setDescription(''); + setSelectedDatasetId(datasetId || ''); + setSuccess(true); + setTimeout(() => { + setSuccess(false); + setIsOpen(false); + // Refresh the page to show the new experiment + window.location.reload(); + }, 1500); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to create experiment'); + } finally { + setLoading(false); + } + } + + function handleClose() { + if (!loading) { + setIsOpen(false); + setName(''); + setDescription(''); + setSelectedDatasetId(datasetId || ''); + setError(null); + setSuccess(false); + } + } + + return ( + <> + + + { + const form = document.getElementById('experimentForm') as HTMLFormElement; + form?.dispatchEvent(new Event('submit', { bubbles: true })); + }, + variant: 'primary', + disabled: loading, + }, + ]} + > +
+
+ + setName(e.target.value)} + placeholder="Enter experiment name" + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={loading} + autoFocus + /> +
+ +
+ +