added backend-mgmt for dataset/dataset_entry and utterances, this includes upload and also displaying of the dataset on the admin pages
This commit is contained in:
46
src/app/actions/datasets.ts
Normal file
46
src/app/actions/datasets.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset } from '@/lib/model/dataset';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function createDataset({ name }: { name: string }) {
|
||||
if (!name || !name.trim()) {
|
||||
throw new Error('Dataset name is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db.insert(dataset).values({ name: name.trim() });
|
||||
revalidatePath('/admin');
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error creating dataset:', error);
|
||||
throw new Error('Failed to create dataset');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDataset(
|
||||
id: number,
|
||||
updates: { name?: string }
|
||||
) {
|
||||
if (updates.name !== undefined && !updates.name.trim()) {
|
||||
throw new Error('Dataset name cannot be empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await db
|
||||
.update(dataset)
|
||||
.set({
|
||||
name: updates.name?.trim(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(dataset.id, id));
|
||||
revalidatePath(`/admin/datasets/${id}`);
|
||||
revalidatePath('/admin');
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error updating dataset:', error);
|
||||
throw new Error('Failed to update dataset');
|
||||
}
|
||||
}
|
||||
70
src/app/actions/get-dataset-entries.ts
Normal file
70
src/app/actions/get-dataset-entries.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { count, eq, and, like } from 'drizzle-orm';
|
||||
|
||||
const ENTRIES_PER_PAGE = 20;
|
||||
|
||||
interface FilterParams {
|
||||
speakerId?: string;
|
||||
modelName?: string;
|
||||
dialect?: string;
|
||||
iteration?: number;
|
||||
utteranceId?: string;
|
||||
}
|
||||
|
||||
export async function getDatasetEntries(
|
||||
datasetId: number,
|
||||
page: number = 1,
|
||||
filters?: FilterParams
|
||||
) {
|
||||
const offset = (page - 1) * ENTRIES_PER_PAGE;
|
||||
|
||||
// Build where conditions
|
||||
const conditions = [eq(dataset_entry.datasetId, datasetId)];
|
||||
|
||||
if (filters?.speakerId) {
|
||||
conditions.push(like(dataset_entry.speakerId, `%${filters.speakerId}%`));
|
||||
}
|
||||
|
||||
if (filters?.modelName) {
|
||||
conditions.push(like(dataset_entry.modelName, `%${filters.modelName}%`));
|
||||
}
|
||||
|
||||
if (filters?.dialect) {
|
||||
conditions.push(like(dataset_entry.dialect, `%${filters.dialect}%`));
|
||||
}
|
||||
|
||||
if (filters?.iteration !== undefined) {
|
||||
conditions.push(eq(dataset_entry.iteration, filters.iteration));
|
||||
}
|
||||
|
||||
if (filters?.utteranceId) {
|
||||
conditions.push(eq(dataset_entry.utteranceId, filters.utteranceId));
|
||||
}
|
||||
|
||||
const whereClause = and(...conditions);
|
||||
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(dataset_entry)
|
||||
.where(whereClause)
|
||||
.limit(ENTRIES_PER_PAGE)
|
||||
.offset(offset);
|
||||
|
||||
const totalResult = await db
|
||||
.select({ count: count() })
|
||||
.from(dataset_entry)
|
||||
.where(whereClause);
|
||||
|
||||
const total = totalResult[0]?.count || 0;
|
||||
const hasMore = offset + ENTRIES_PER_PAGE < total;
|
||||
|
||||
return {
|
||||
entries,
|
||||
hasMore,
|
||||
total,
|
||||
page,
|
||||
};
|
||||
}
|
||||
27
src/app/actions/get-filter-options.ts
Normal file
27
src/app/actions/get-filter-options.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function getFilterOptions(datasetId: number) {
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(dataset_entry)
|
||||
.where(eq(dataset_entry.datasetId, datasetId));
|
||||
|
||||
// Extract unique values for each filter
|
||||
const speakerIds = [...new Set(entries.map(e => e.speakerId))].sort();
|
||||
const modelNames = [...new Set(entries.map(e => e.modelName))].sort();
|
||||
const dialects = [...new Set(entries.map(e => e.dialect))].sort();
|
||||
const iterations = [...new Set(entries.map(e => e.iteration))].sort((a, b) => a - b);
|
||||
const utteranceIds = [...new Set(entries.map(e => e.utteranceId).filter(Boolean))].sort();
|
||||
|
||||
return {
|
||||
speakerIds,
|
||||
modelNames,
|
||||
dialects,
|
||||
iterations,
|
||||
utteranceIds,
|
||||
};
|
||||
}
|
||||
34
src/app/actions/remove-dataset-entries.ts
Normal file
34
src/app/actions/remove-dataset-entries.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { rm } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
export async function removeAllDatasetEntries(datasetId: number) {
|
||||
try {
|
||||
// Delete all entries from database
|
||||
await db.delete(dataset_entry).where(eq(dataset_entry.datasetId, datasetId));
|
||||
|
||||
// Delete dataset files from public folder
|
||||
const datasetDir = join(process.cwd(), 'public', 'datasets', datasetId.toString());
|
||||
if (existsSync(datasetDir)) {
|
||||
await rm(datasetDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
revalidatePath(`/admin/datasets/${datasetId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'All dataset entries have been removed',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error removing dataset entries:', error);
|
||||
throw new Error(
|
||||
error instanceof Error ? error.message : 'Failed to remove dataset entries'
|
||||
);
|
||||
}
|
||||
}
|
||||
119
src/app/actions/upload-utterances.ts
Normal file
119
src/app/actions/upload-utterances.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset_utterance } from '@/lib/model/utterance';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
export async function uploadDatasetUtterances(
|
||||
datasetId: number,
|
||||
formData: FormData
|
||||
) {
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
throw new Error('No file provided');
|
||||
}
|
||||
|
||||
if (!file.name.endsWith('.csv')) {
|
||||
throw new Error('File must be a CSV file');
|
||||
}
|
||||
|
||||
try {
|
||||
const csvContent = await file.text();
|
||||
const lines = csvContent.split('\n').filter((line) => line.trim());
|
||||
|
||||
if (lines.length < 2) {
|
||||
throw new Error('CSV is empty or invalid');
|
||||
}
|
||||
|
||||
// Parse CSV header
|
||||
const headers = lines[0].split(',').map((h) => h.trim());
|
||||
|
||||
if (!headers.includes('id') || !headers.includes('text')) {
|
||||
throw new Error('CSV must have "id" and "text" columns');
|
||||
}
|
||||
|
||||
// Get indices of id and text columns
|
||||
const idIndex = headers.indexOf('id');
|
||||
const textIndex = headers.indexOf('text');
|
||||
|
||||
// Parse utterances
|
||||
const utterances: typeof dataset_utterance.$inferInsert[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
// Handle CSV parsing with quotes
|
||||
const line = lines[i];
|
||||
const parts: string[] = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j];
|
||||
const nextChar = line[j + 1];
|
||||
|
||||
if (char === '"') {
|
||||
if (inQuotes && nextChar === '"') {
|
||||
current += '"';
|
||||
j++; // Skip next quote
|
||||
} else {
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
parts.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
parts.push(current.trim());
|
||||
|
||||
if (parts.length > Math.max(idIndex, textIndex)) {
|
||||
const id = parts[idIndex]?.trim();
|
||||
const text = parts[textIndex]?.trim();
|
||||
|
||||
if (id && text) {
|
||||
utterances.push({
|
||||
datasetId,
|
||||
id,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (utterances.length === 0) {
|
||||
throw new Error('No valid utterances found in CSV');
|
||||
}
|
||||
|
||||
// Check for existing utterances to avoid duplicates
|
||||
const existingUtterances = await db
|
||||
.select({ id: dataset_utterance.id })
|
||||
.from(dataset_utterance)
|
||||
.where(eq(dataset_utterance.datasetId, datasetId));
|
||||
|
||||
const existingIds = new Set(existingUtterances.map((u) => u.id));
|
||||
|
||||
// Filter out duplicates
|
||||
const newUtterances = utterances.filter((u) => !existingIds.has(u.id));
|
||||
|
||||
if (newUtterances.length === 0) {
|
||||
throw new Error('All utterances already exist in the dataset');
|
||||
}
|
||||
|
||||
// Insert new utterances into database
|
||||
await db.insert(dataset_utterance).values(newUtterances);
|
||||
|
||||
revalidatePath(`/admin/datasets/${datasetId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
utterancesCreated: newUtterances.length,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error uploading utterances:', error);
|
||||
throw new Error(
|
||||
error instanceof Error ? error.message : 'Failed to upload utterances'
|
||||
);
|
||||
}
|
||||
}
|
||||
157
src/app/actions/upload.ts
Normal file
157
src/app/actions/upload.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
'use server';
|
||||
|
||||
import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { dataset_utterance } from '@/lib/model/utterance';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { writeFile, mkdir, readFile, rm, readdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import extract from 'extract-zip';
|
||||
|
||||
interface DatasetEntryInput {
|
||||
id: string;
|
||||
audio_file: string;
|
||||
duration_ms: string;
|
||||
utt_id: string;
|
||||
speaker: string;
|
||||
model: string;
|
||||
dialect: string;
|
||||
iteration: string;
|
||||
}
|
||||
|
||||
export async function uploadDatasetEntries(
|
||||
datasetId: number,
|
||||
formData: FormData
|
||||
) {
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
if (!file) {
|
||||
throw new Error('No file provided');
|
||||
}
|
||||
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
throw new Error('File must be a ZIP archive');
|
||||
}
|
||||
|
||||
const tempDir = join(process.cwd(), 'tmp', `upload-${Date.now()}`);
|
||||
const datasetDir = join(process.cwd(), 'public', 'datasets', datasetId.toString());
|
||||
|
||||
try {
|
||||
// Create temp directory
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
|
||||
// Save uploaded file to temp location
|
||||
const tempZipPath = join(tempDir, file.name);
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
await writeFile(tempZipPath, buffer);
|
||||
|
||||
// Extract ZIP file
|
||||
await extract( tempZipPath, { dir: tempDir } );
|
||||
|
||||
// Read metadata.csv
|
||||
const metadataPath = join(tempDir, 'metadata.csv');
|
||||
if (!existsSync(metadataPath)) {
|
||||
throw new Error('metadata.csv not found in ZIP');
|
||||
}
|
||||
|
||||
const metadataContent = await readFile(metadataPath, 'utf-8');
|
||||
const lines = metadataContent.split('\n').filter(line => line.trim());
|
||||
|
||||
if (lines.length < 2) {
|
||||
throw new Error('metadata.csv is empty or invalid');
|
||||
}
|
||||
|
||||
// Parse CSV header
|
||||
const headers = lines[0].split(',').map(h => h.trim());
|
||||
|
||||
// Create dataset directory
|
||||
await mkdir(datasetDir, { recursive: true });
|
||||
|
||||
// Parse and insert entries
|
||||
const entries: typeof dataset_entry.$inferInsert[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = lines[i].split(',').map(v => v.trim());
|
||||
|
||||
if (values.length !== headers.length) {
|
||||
continue; // Skip malformed lines
|
||||
}
|
||||
|
||||
const row: Record<string, string> = {};
|
||||
headers.forEach((header, index) => {
|
||||
row[header] = values[index];
|
||||
});
|
||||
|
||||
const audioFile = row.audio_file as string;
|
||||
// Try different path separators and remove /output prefix
|
||||
const relativePath = audioFile.replace(/\\/g, '/').replace(/^output\//, '');
|
||||
const audioPath = join(tempDir, relativePath);
|
||||
|
||||
if (existsSync(audioPath)) {
|
||||
// Copy audio file to dataset directory with external ID as filename
|
||||
const audioBuffer = await readFile(audioPath);
|
||||
const fileExtension = audioFile.substring(audioFile.lastIndexOf('.'));
|
||||
const destPath = join(datasetDir, `${row.id}${fileExtension}`);
|
||||
|
||||
await writeFile(destPath, audioBuffer);
|
||||
|
||||
entries.push({
|
||||
datasetId,
|
||||
externalId: row.id,
|
||||
speakerId: row.speaker,
|
||||
modelName: row.model,
|
||||
fileName: relativePath,
|
||||
utteranceId: row.utt_id || undefined,
|
||||
dialect: row.dialect,
|
||||
iteration: parseInt(row.iteration, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
throw new Error('No valid entries found in metadata.csv');
|
||||
}
|
||||
|
||||
// Check for existing entries to avoid duplicates
|
||||
const existingEntries = await db
|
||||
.select({ externalId: dataset_entry.externalId })
|
||||
.from(dataset_entry)
|
||||
.where(eq(dataset_entry.datasetId, datasetId));
|
||||
|
||||
const existingExternalIds = new Set(existingEntries.map(e => e.externalId));
|
||||
|
||||
// Filter out duplicates
|
||||
const newEntries = entries.filter(entry => !existingExternalIds.has(entry.externalId));
|
||||
|
||||
if (newEntries.length === 0) {
|
||||
throw new Error('All entries already exist in the dataset');
|
||||
}
|
||||
|
||||
// Insert new entries into database
|
||||
await db.insert(dataset_entry).values(newEntries);
|
||||
|
||||
revalidatePath(`/admin/datasets/${datasetId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
entriesCreated: entries.length,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error uploading dataset entries:', error);
|
||||
throw new Error(
|
||||
error instanceof Error ? error.message : 'Failed to upload dataset entries'
|
||||
);
|
||||
} finally {
|
||||
// Clean up temp directory
|
||||
try {
|
||||
if (existsSync(tempDir)) {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('Error cleaning up temp directory:', cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
src/app/admin/datasets/[id]/entries/[entryId]/page.tsx
Normal file
135
src/app/admin/datasets/[id]/entries/[entryId]/page.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import Link from 'next/link';
|
||||
import db from '@/lib/db';
|
||||
import { dataset_entry } from '@/lib/model/dataset_entry';
|
||||
import { dataset_utterance } from '@/lib/model/utterance';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import AudioPlayer from '@/components/AudioPlayer';
|
||||
|
||||
interface DatasetEntryPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
entryId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DatasetEntryPage({ params }: DatasetEntryPageProps) {
|
||||
const { id: datasetId, entryId } = await params;
|
||||
const datasetIdNum = parseInt(datasetId, 10);
|
||||
const entryIdNum = parseInt(entryId, 10);
|
||||
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(dataset_entry)
|
||||
.where(eq(dataset_entry.id, entryIdNum));
|
||||
|
||||
if (entries.length === 0 || entries[0].datasetId !== datasetIdNum) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Link
|
||||
href={`/admin/datasets/${datasetId}`}
|
||||
className="text-blue-500 hover:text-blue-600 mb-6 inline-block"
|
||||
>
|
||||
← Back to Dataset
|
||||
</Link>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-gray-600">Dataset entry not found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const entry = entries[0];
|
||||
|
||||
// Fetch utterance if utteranceId is present
|
||||
let utterance = null;
|
||||
if (entry.utteranceId) {
|
||||
const utterances = await db
|
||||
.select()
|
||||
.from(dataset_utterance)
|
||||
.where(eq(dataset_utterance.id, entry.utteranceId));
|
||||
|
||||
utterance = utterances.length > 0 ? utterances[0] : null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<Link
|
||||
href={`/admin/datasets/${datasetId}`}
|
||||
className="text-blue-500 hover:text-blue-600 mb-6 inline-block"
|
||||
>
|
||||
← Back to Dataset
|
||||
</Link>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h1 className="text-3xl font-bold mb-6">Dataset Entry Details</h1>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Entry ID</p>
|
||||
<p className="text-lg font-semibold">{entry.id}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">External ID</p>
|
||||
<p className="text-lg font-semibold">{entry.externalId}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Speaker ID</p>
|
||||
<p className="text-lg font-semibold">{entry.speakerId}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Model Name</p>
|
||||
<p className="text-lg font-semibold">{entry.modelName}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Dialect</p>
|
||||
<p className="text-lg font-semibold">{entry.dialect}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Iteration</p>
|
||||
<p className="text-lg font-semibold">{entry.iteration}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">File Name</p>
|
||||
<p className="text-lg font-semibold break-all">{entry.fileName}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-3">Audio Player</p>
|
||||
<AudioPlayer datasetId={datasetIdNum} fileName={entry.fileName} externalId={entry.externalId} />
|
||||
</div>
|
||||
|
||||
{utterance && (
|
||||
<div className="col-span-2 bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<p className="text-sm font-semibold text-gray-700 mb-2">Utterance {entry.utteranceId}</p>
|
||||
<p className="text-lg text-gray-800 leading-relaxed">{utterance.text}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Created</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{new Date(entry.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-gray-200 p-4 rounded-lg">
|
||||
<p className="text-sm text-gray-600 mb-1">Last Updated</p>
|
||||
<p className="text-lg font-semibold">
|
||||
{new Date(entry.updatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
68
src/app/admin/datasets/[id]/page.tsx
Normal file
68
src/app/admin/datasets/[id]/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import Link from 'next/link';
|
||||
import db from '@/lib/db';
|
||||
import { dataset } from '@/lib/model/dataset';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import EditableDatasetHeader from '@/components/EditableDatasetHeader';
|
||||
import DatasetEntriesList from '@/components/DatasetEntriesList';
|
||||
import UploadDatasetEntriesModal from '@/components/UploadDatasetEntriesModal';
|
||||
import UploadUtterancesModal from '@/components/UploadUtterancesModal';
|
||||
import RemoveAllEntriesButton from '@/components/RemoveAllEntriesButton';
|
||||
|
||||
interface DatasetPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DatasetPage({ params }: DatasetPageProps) {
|
||||
const {id} = await params;
|
||||
const datasetId = parseInt(id, 10);
|
||||
|
||||
const datasets = await db
|
||||
.select()
|
||||
.from(dataset)
|
||||
.where(eq(dataset.id, datasetId));
|
||||
|
||||
if (datasets.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="max-w-7xl mx-auto 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="bg-white rounded-lg shadow p-6">
|
||||
<p className="text-gray-600">Dataset not found</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ds = datasets[0];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-7xl mx-auto 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>
|
||||
|
||||
<EditableDatasetHeader dataset={ds} />
|
||||
|
||||
<div className="flex gap-3 mb-6">
|
||||
<UploadDatasetEntriesModal datasetId={datasetId} />
|
||||
<UploadUtterancesModal datasetId={datasetId} />
|
||||
<RemoveAllEntriesButton datasetId={datasetId} />
|
||||
</div>
|
||||
|
||||
<DatasetEntriesList datasetId={datasetId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import AuthRedirect from "@/components/AuthRedirect";
|
||||
import DatasetsList from "@/components/DatasetsList";
|
||||
import CreateDatasetModal from "@/components/CreateDatasetModal";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function AdminPage() {
|
||||
export default async function AdminPage() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers()
|
||||
})
|
||||
|
||||
if(!session) {
|
||||
redirect("/user/sign-in")
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AuthRedirect />
|
||||
<h1>Admin Dashboard</h1>
|
||||
<p>Welcome to the admin dashboard. Here you can manage the application.</p>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<CreateDatasetModal />
|
||||
<DatasetsList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ message: "Hello world!" });
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import './globals.css';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { AudioProvider } from '@/components/AudioProvider';
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@@ -10,11 +11,13 @@ export default function RootLayout({
|
||||
return (
|
||||
<html>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-grow mt-4 mb-4 mr-8 ml-8">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<AudioProvider>
|
||||
<Header />
|
||||
<main className="flex-grow mt-4 mb-4 mr-8 ml-8">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</AudioProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user