feat: Add admin experiment management pages and components
- Implemented ExperimentPage for viewing and editing individual experiments. - Created ExperimentsAdminPage for managing all experiments with a list and a modal for creating new experiments. - Developed AnnotatePage for annotating experiments with different views based on annotation tools. - Added CreateExperimentModal for creating new experiments with dataset selection. - Introduced DeleteExperimentButton for confirming and executing experiment deletions. - Built EditableExperimentHeader for editing experiment details. - Created ExperimentsList component to display a list of experiments with links to their details.
This commit is contained in:
2
drizzle/0010_luxuriant_jigsaw.sql
Normal file
2
drizzle/0010_luxuriant_jigsaw.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `experiment` ADD `published` integer DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE `experiment` ADD `annotation_tool` text;
|
||||||
748
drizzle/meta/0010_snapshot.json
Normal file
748
drizzle/meta/0010_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,13 @@
|
|||||||
"when": 1773985687853,
|
"when": 1773985687853,
|
||||||
"tag": "0009_mysterious_brother_voodoo",
|
"tag": "0009_mysterious_brother_voodoo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1773987751513,
|
||||||
|
"tag": "0010_luxuriant_jigsaw",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export async function saveAnnotations(
|
|||||||
|
|
||||||
/** Returns all datasets (for the home page). */
|
/** Returns all datasets (for the home page). */
|
||||||
export async function getAllExperiments() {
|
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. */
|
/** Returns annotation progress for the current user in a dataset. */
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ import { revalidatePath } from 'next/cache';
|
|||||||
import { removeAllDatasetEntries } from './remove-dataset-entries';
|
import { removeAllDatasetEntries } from './remove-dataset-entries';
|
||||||
import { requireAdmin } from '@/lib/auth';
|
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 }) {
|
export async function createDataset({ name, description }: { name: string; description?: string }) {
|
||||||
const result = await requireAdmin();
|
const result = await requireAdmin();
|
||||||
if (!result.authenticated || !result.admin) {
|
if (!result.authenticated || !result.admin) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
100
src/app/admin/experiments/[id]/page.tsx
Normal file
100
src/app/admin/experiments/[id]/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<Link
|
||||||
|
href="/admin/experiments"
|
||||||
|
className="text-blue-500 hover:text-blue-600 mb-6 inline-block"
|
||||||
|
>
|
||||||
|
← Back to Experiments
|
||||||
|
</Link>
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<p className="text-gray-600">Experiment not found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exp = experiments[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<Link
|
||||||
|
href="/admin/experiments"
|
||||||
|
className="text-blue-500 hover:text-blue-600 mb-6 inline-block"
|
||||||
|
>
|
||||||
|
← Back to Experiments
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<EditableExperimentHeader experiment={exp} />
|
||||||
|
|
||||||
|
<div className="flex gap-3 mb-6">
|
||||||
|
<DeleteExperimentButton experimentId={experimentId} experimentName={exp.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Experiment Details</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Experiment ID</p>
|
||||||
|
<p className="text-lg font-semibold">{exp.id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Dataset ID</p>
|
||||||
|
<p className="text-lg font-semibold">{exp.datasetId}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Created</p>
|
||||||
|
<p className="text-lg font-semibold">{new Date(exp.createdAt).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Last Updated</p>
|
||||||
|
<p className="text-lg font-semibold">{new Date(exp.updatedAt).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Annotation Tool</p>
|
||||||
|
<p className="text-lg font-semibold">{exp.annotationTool}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/app/admin/experiments/page.tsx
Normal file
37
src/app/admin/experiments/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<Link
|
||||||
|
href="/admin"
|
||||||
|
className="text-blue-500 hover:text-blue-600 mb-6 inline-block"
|
||||||
|
>
|
||||||
|
← Back to Admin
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold mb-4">Experiments Management</h1>
|
||||||
|
<p className="text-gray-600">Manage all experiments across datasets</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CreateExperimentModal />
|
||||||
|
|
||||||
|
<ExperimentsList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import DatasetsList from "@/components/DatasetsList";
|
import DatasetsList from "@/components/DatasetsList";
|
||||||
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
||||||
|
import ExperimentsList from "@/components/ExperimentsList";
|
||||||
import { AdminTokenForm } from "@/components/AdminTokenForm";
|
import { AdminTokenForm } from "@/components/AdminTokenForm";
|
||||||
import BuildInfo from "@/components/BuildInfo";
|
import BuildInfo from "@/components/BuildInfo";
|
||||||
import { requireAdmin } from "@/lib/auth";
|
import { requireAdmin } from "@/lib/auth";
|
||||||
@@ -28,6 +29,12 @@ export default async function AdminPage() {
|
|||||||
>
|
>
|
||||||
Manage Users
|
Manage Users
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/admin/experiments"
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Manage Experiments
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,6 +47,12 @@ export default async function AdminPage() {
|
|||||||
<CreateDatasetModal />
|
<CreateDatasetModal />
|
||||||
<DatasetsList />
|
<DatasetsList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Experiments</h2>
|
||||||
|
<p className="text-gray-600 mb-4">Manage experiments globally or <Link href="/admin/experiments" className="text-blue-500 hover:text-blue-600 font-semibold">go to the experiments management page</Link></p>
|
||||||
|
<ExperimentsList />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div className="max-w-2xl mx-auto py-8">
|
|
||||||
<Link href="/" className="text-sm text-gray-500 hover:text-gray-700 mb-6 inline-block">
|
|
||||||
← Zurück zur Startseite
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">Prototyp auswählen</h1>
|
|
||||||
<p className="text-gray-500 mb-8">
|
|
||||||
Wählen Sie einen Annotationsprototyp aus, um mit der Bewertung zu beginnen.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{PROTOTYPES.map((proto) => (
|
|
||||||
<Link
|
|
||||||
key={proto.id}
|
|
||||||
href={`/annotate/${datasetId}/${proto.id}`}
|
|
||||||
className="bg-white border border-gray-200 rounded-xl p-5 shadow-sm hover:border-blue-300 hover:shadow-md transition-all"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h2 className="font-semibold text-gray-800">{proto.name}</h2>
|
|
||||||
<p className="text-sm text-gray-500 mt-0.5">{proto.description}</p>
|
|
||||||
</div>
|
|
||||||
<span className="text-gray-400 text-lg">→</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,17 +4,19 @@ import Link from 'next/link';
|
|||||||
import { auth } from '@/lib/auth';
|
import { auth } from '@/lib/auth';
|
||||||
import { getAnnotationEntries } from '@/app/actions/annotations';
|
import { getAnnotationEntries } from '@/app/actions/annotations';
|
||||||
import SingleChoiceView from '@/components/AnnotationViews/SingleChoiceView';
|
import SingleChoiceView from '@/components/AnnotationViews/SingleChoiceView';
|
||||||
|
import { getExperimentById } from '@/app/actions/experiment';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: Promise<{ experimentId: string; prototype: string }>;
|
params: Promise<{ experimentId: string}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function AnnotatePage({ params }: Props) {
|
export default async function AnnotatePage({ params }: Props) {
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
if (!session) redirect('/user/sign-in');
|
if (!session) redirect('/user/sign-in');
|
||||||
|
|
||||||
const { experimentId: experimentIdStr, prototype } = await params;
|
const { experimentId: experimentIdStr } = await params;
|
||||||
const experimentId = parseInt(experimentIdStr, 10);
|
const experimentId = parseInt(experimentIdStr, 10);
|
||||||
|
const prototype = await getExperimentById(experimentId).then(exp => exp?.annotationTool || null);
|
||||||
|
|
||||||
if (isNaN(experimentId)) {
|
if (isNaN(experimentId)) {
|
||||||
return (
|
return (
|
||||||
@@ -27,9 +29,6 @@ export default async function AnnotatePage({ params }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prototype !== 'single-choice') {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await getAnnotationEntries(experimentId);
|
const entries = await getAnnotationEntries(experimentId);
|
||||||
|
|
||||||
@@ -53,5 +52,23 @@ export default async function AnnotatePage({ params }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SingleChoiceView entries={entries} experimentId={experimentId} />;
|
// Render based on prototype type
|
||||||
|
const renderAnnotationView = () => {
|
||||||
|
switch (prototype) {
|
||||||
|
case 'single-choice':
|
||||||
|
return <SingleChoiceView entries={entries} experimentId={experimentId} />;
|
||||||
|
// Add more prototypes here as needed
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<div className="max-w-xl mx-auto py-16 text-center">
|
||||||
|
<p className="text-gray-600">Unbekannter Annotation-Typ: {prototype}</p>
|
||||||
|
<Link href="/" className="text-blue-600 hover:underline mt-4 inline-block">
|
||||||
|
← Startseite
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderAnnotationView();
|
||||||
}
|
}
|
||||||
206
src/components/CreateExperimentModal.tsx
Normal file
206
src/components/CreateExperimentModal.tsx
Normal file
@@ -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<number | ''>(datasetId || '');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [datasetsLoading, setDatasetsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const [datasets, setDatasets] = useState<Dataset[]>([]);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="mb-6 px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors"
|
||||||
|
>
|
||||||
|
+ Create New Experiment
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
title="Create New Experiment"
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
onClick: handleClose,
|
||||||
|
variant: 'secondary',
|
||||||
|
disabled: loading,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: loading ? 'Creating...' : 'Create',
|
||||||
|
onClick: () => {
|
||||||
|
const form = document.getElementById('experimentForm') as HTMLFormElement;
|
||||||
|
form?.dispatchEvent(new Event('submit', { bubbles: true }));
|
||||||
|
},
|
||||||
|
variant: 'primary',
|
||||||
|
disabled: loading,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<form id="experimentForm" onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||||
|
Experiment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => 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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
||||||
|
Description (Optional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Enter experiment description"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||||
|
rows={3}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!datasetId && (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="datasetId" className="block text-sm font-medium mb-2">
|
||||||
|
Dataset
|
||||||
|
</label>
|
||||||
|
{datasetsLoading ? (
|
||||||
|
<div className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500">
|
||||||
|
Loading datasets...
|
||||||
|
</div>
|
||||||
|
) : datasets.length === 0 ? (
|
||||||
|
<div className="w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-500">
|
||||||
|
No datasets available
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
id="datasetId"
|
||||||
|
value={selectedDatasetId}
|
||||||
|
onChange={(e) => setSelectedDatasetId(e.target.value ? Number(e.target.value) : '')}
|
||||||
|
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}
|
||||||
|
>
|
||||||
|
<option value="">-- Select a Dataset --</option>
|
||||||
|
{datasets.map((ds) => (
|
||||||
|
<option key={ds.id} value={ds.id}>
|
||||||
|
{ds.name} (ID: {ds.id})
|
||||||
|
{ds.description ? ` - ${ds.description}` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Select the dataset for this experiment</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="p-3 bg-green-100 border border-green-400 text-green-700 rounded text-sm">
|
||||||
|
Experiment created successfully!
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
src/components/DeleteExperimentButton.tsx
Normal file
81
src/components/DeleteExperimentButton.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { deleteExperiment } from '@/app/actions/experiment';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
interface DeleteExperimentButtonProps {
|
||||||
|
experimentId: number;
|
||||||
|
experimentName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeleteExperimentButton({ experimentId, experimentName }: DeleteExperimentButtonProps) {
|
||||||
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function handleConfirm() {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteExperiment(experimentId);
|
||||||
|
setIsConfirming(false);
|
||||||
|
// Redirect to experiments page after deletion
|
||||||
|
router.push('/admin/experiments');
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to delete experiment';
|
||||||
|
setError(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfirming) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-6 max-w-sm">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Delete experiment "{experimentName}"?
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
This will permanently delete the experiment. This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsConfirming(false)}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 disabled:bg-gray-200"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{loading ? 'Deleting...' : 'Delete'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsConfirming(true)}
|
||||||
|
className="px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600"
|
||||||
|
>
|
||||||
|
Delete Experiment
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
168
src/components/EditableExperimentHeader.tsx
Normal file
168
src/components/EditableExperimentHeader.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { updateExperiment } from '@/app/actions/experiment';
|
||||||
|
import { Experiment } from '@/lib/model/experiment';
|
||||||
|
|
||||||
|
interface EditableExperimentHeaderProps {
|
||||||
|
experiment: Experiment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANNOTATION_TOOLS = [
|
||||||
|
{ value: 'single-choice', label: 'Single Choice' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EditableExperimentHeader({
|
||||||
|
experiment,
|
||||||
|
}: EditableExperimentHeaderProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [name, setName] = useState(experiment.name);
|
||||||
|
const [description, setDescription] = useState(experiment.description || '');
|
||||||
|
const [annotationTool, setAnnotationTool] = useState(experiment.annotationTool || 'single-choice');
|
||||||
|
const [published, setPublished] = useState(experiment.published || false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!name.trim()) {
|
||||||
|
setError('Experiment name cannot be empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateExperiment(experiment.id, {
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim() || undefined,
|
||||||
|
annotationTool: annotationTool,
|
||||||
|
published: published
|
||||||
|
});
|
||||||
|
setIsEditing(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to update experiment');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
setName(experiment.name);
|
||||||
|
setAnnotationTool(experiment.annotationTool || 'single-choice');
|
||||||
|
setPublished(experiment.published || false);
|
||||||
|
setDescription(experiment.description || '');
|
||||||
|
setError(null);
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
|
{isEditing ? (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||||
|
Experiment Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium mb-2">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none mb-4"
|
||||||
|
rows={3}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label htmlFor="annotationTool" className="block text-sm font-medium mb-2">
|
||||||
|
Annotation Tool
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="annotationTool"
|
||||||
|
value={annotationTool}
|
||||||
|
onChange={(e) => setAnnotationTool(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 mb-4"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{ANNOTATION_TOOLS.map((tool) => (
|
||||||
|
<option key={tool.value} value={tool.value}>
|
||||||
|
{tool.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<input
|
||||||
|
id="published"
|
||||||
|
type="checkbox"
|
||||||
|
checked={published}
|
||||||
|
onChange={(e) => setPublished(e.target.checked)}
|
||||||
|
className="w-4 h-4 text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<label htmlFor="published" className="ml-2 text-sm font-medium text-gray-700">
|
||||||
|
Published
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400"
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : 'Save'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400 disabled:bg-gray-200"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold mb-2">{experiment.name}</h1>
|
||||||
|
<p className="text-lg text-gray-600 mb-4">{experiment.description}</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
ID: {experiment.id} | Dataset ID: {experiment.datasetId}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
Annotation Tool: {ANNOTATION_TOOLS.find(t => t.value === (experiment.annotationTool || 'single-choice'))?.label || 'Single Choice'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
Status: {experiment.published ? 'Published' : 'Draft'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
Created: {new Date(experiment.createdAt).toLocaleDateString()} |
|
||||||
|
Updated: {new Date(experiment.updatedAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
src/components/ExperimentsList.tsx
Normal file
41
src/components/ExperimentsList.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import db from '@/lib/db';
|
||||||
|
import { experiment } from '@/lib/model/experiment';
|
||||||
|
import { dataset } from '@/lib/model/dataset';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default async function ExperimentsList() {
|
||||||
|
const experiments = await db
|
||||||
|
.select()
|
||||||
|
.from(experiment)
|
||||||
|
.orderBy(experiment.createdAt);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-8">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Experiments</h2>
|
||||||
|
{experiments.length === 0 ? (
|
||||||
|
<p className="text-gray-500">No experiments found.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{experiments.map((exp) => (
|
||||||
|
<Link
|
||||||
|
key={exp.id}
|
||||||
|
href={`/admin/experiments/${exp.id}`}
|
||||||
|
className="border border-gray-300 rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition-shadow block cursor-pointer"
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold">{exp.name}</h3>
|
||||||
|
<p className="text-md text-gray-600">{exp.description}</p>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
ID: {exp.id} | Dataset ID: {exp.datasetId}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
Created: {new Date(exp.createdAt).toLocaleDateString()}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ export const experiment = sqliteTable(
|
|||||||
{
|
{
|
||||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||||
name: text('name').notNull(),
|
name: text('name').notNull(),
|
||||||
|
published: integer('published', { mode: 'boolean' }).notNull().default(false), // 0 = draft, 1 = published
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
|
annotationTool: text('annotation_tool'),
|
||||||
datasetId: integer('dataset_id')
|
datasetId: integer('dataset_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => dataset.id),
|
.references(() => dataset.id),
|
||||||
|
|||||||
Reference in New Issue
Block a user