feat: add description field to dataset and update related components for dataset creation and editing

This commit is contained in:
averel10
2026-03-13 10:17:17 +01:00
parent 209ca6a6c0
commit 6432375a4f
8 changed files with 589 additions and 9 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE `dataset` ADD `description` text;

View File

@@ -0,0 +1,521 @@
{
"version": "6",
"dialect": "sqlite",
"id": "dade89a0-2dca-4d30-afd6-7b1c36089481",
"prevId": "323c1657-def5-448b-8f9b-80c869eb3000",
"tables": {
"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
},
"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
},
"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": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -15,6 +15,13 @@
"when": 1773389231869,
"tag": "0001_friendly_tana_nile",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1773393053497,
"tag": "0002_foamy_quicksilver",
"breakpoints": true
}
]
}

View File

@@ -6,7 +6,7 @@ import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { requireAdmin } from '@/lib/auth';
export async function createDataset({ name }: { name: string }) {
export async function createDataset({ name, description }: { name: string; description?: string }) {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
throw new Error('Unauthorized');
@@ -17,7 +17,10 @@ export async function createDataset({ name }: { name: string }) {
}
try {
const result = await db.insert(dataset).values({ name: name.trim() });
const result = await db.insert(dataset).values({
name: name.trim(),
description: description?.trim() || null
});
revalidatePath('/admin');
return result;
} catch (error) {
@@ -28,7 +31,7 @@ export async function createDataset({ name }: { name: string }) {
export async function updateDataset(
id: number,
updates: { name?: string }
updates: { name?: string; description?: string }
) {
const result = await requireAdmin();
if (!result.authenticated || !result.admin) {
@@ -40,12 +43,18 @@ export async function updateDataset(
}
try {
const updateData: any = {};
if (updates.name !== undefined) {
updateData.name = updates.name.trim();
}
if (updates.description !== undefined) {
updateData.description = updates.description.trim() || null;
}
updateData.updatedAt = new Date();
const result = await db
.update(dataset)
.set({
name: updates.name?.trim(),
updatedAt: new Date(),
})
.set(updateData)
.where(eq(dataset.id, id));
revalidatePath(`/admin/datasets/${id}`);
revalidatePath('/admin');

View File

@@ -5,6 +5,7 @@ import { createDataset } from '@/app/actions/datasets';
export default function CreateDatasetForm() {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
@@ -21,8 +22,12 @@ export default function CreateDatasetForm() {
setLoading(true);
try {
await createDataset({ name: name.trim() });
await createDataset({
name: name.trim(),
description: description.trim() || undefined
});
setName('');
setDescription('');
setSuccess(true);
setTimeout(() => setSuccess(false), 3000);
// Refresh the page to show the new dataset
@@ -53,6 +58,21 @@ export default function CreateDatasetForm() {
/>
</div>
<div className="mb-4">
<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 dataset description"
rows={3}
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"
disabled={loading}
/>
</div>
{error && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded">
{error}

View File

@@ -21,6 +21,7 @@ export default async function DatasetsList() {
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">{ds.name}</h3>
<p className="text-md text-gray-600">{ds.description}</p>
<p className="text-sm text-gray-600 mt-2">
ID: {ds.id}
</p>

View File

@@ -13,6 +13,7 @@ export default function EditableDatasetHeader({
}: EditableDatasetHeaderProps) {
const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState(dataset.name);
const [description, setDescription] = useState(dataset.description || '');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -26,7 +27,10 @@ export default function EditableDatasetHeader({
setError(null);
try {
await updateDataset(dataset.id, { name: name.trim() });
await updateDataset(dataset.id, {
name: name.trim(),
description: description.trim() || undefined
});
setIsEditing(false);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update dataset');
@@ -37,6 +41,7 @@ export default function EditableDatasetHeader({
function handleCancel() {
setName(dataset.name);
setDescription(dataset.description || '');
setError(null);
setIsEditing(false);
}
@@ -57,6 +62,18 @@ export default function EditableDatasetHeader({
disabled={loading}
/>
<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)}
rows={3}
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 resize-none"
disabled={loading}
/>
{error && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm">
{error}
@@ -91,6 +108,9 @@ export default function EditableDatasetHeader({
Edit
</button>
</div>
{dataset.description && (
<p className="text-gray-700 mb-4">{dataset.description}</p>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-gray-600">Dataset ID</p>

View File

@@ -6,6 +6,7 @@ import { dataset_entry } from './dataset_entry';
export const dataset = sqliteTable('dataset', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
description: text('description'),
createdAt: integer('created_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`),