293 lines
8.8 KiB
TypeScript
293 lines
8.8 KiB
TypeScript
'use server';
|
|
|
|
import db from '@/lib/db';
|
|
import { experiment } from '@/lib/model/experiment';
|
|
import { experiment_calibration } from '@/lib/model/experiment_calibration';
|
|
import { annotation } from '@/lib/model/annotation';
|
|
import { participant } from '@/lib/model/participant';
|
|
import { dataset_entry } from '@/lib/model/dataset_entry';
|
|
import { and, eq, count, sql, isNotNull } from 'drizzle-orm';
|
|
import { revalidatePath } from 'next/cache';
|
|
import { auth, requireAdmin } from '@/lib/auth';
|
|
import { isParticipantCalibrationDone } from './calibration-scoring';
|
|
import { headers } from 'next/headers';
|
|
|
|
export async function createExperiment({
|
|
name,
|
|
description,
|
|
datasetId,
|
|
annotationTool,
|
|
published = false,
|
|
onboardingEnabled = false,
|
|
calibrationEnabled = false
|
|
}: {
|
|
name: string;
|
|
description?: string;
|
|
datasetId: number;
|
|
annotationTool?: string;
|
|
published?: boolean;
|
|
onboardingEnabled?: boolean;
|
|
calibrationEnabled?: 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,
|
|
onboardingEnabled,
|
|
calibrationEnabled,
|
|
});
|
|
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, onboardingEnabled?: boolean, calibrationEnabled?: 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?.trim();
|
|
}
|
|
if (updates.published !== undefined) {
|
|
updateData.published = updates.published;
|
|
}
|
|
if (updates.onboardingEnabled !== undefined) {
|
|
updateData.onboardingEnabled = updates.onboardingEnabled;
|
|
}
|
|
if (updates.calibrationEnabled !== undefined) {
|
|
updateData.calibrationEnabled = updates.calibrationEnabled;
|
|
}
|
|
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 all experiment_calibration records first
|
|
await db.delete(experiment_calibration).where(eq(experiment_calibration.experimentId, id));
|
|
|
|
// 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');
|
|
}
|
|
}
|
|
|
|
export async function clearExperimentData(id: number) {
|
|
const result = await requireAdmin();
|
|
if (!result.authenticated || !result.admin) {
|
|
throw new Error('Unauthorized');
|
|
}
|
|
|
|
try {
|
|
// Delete all annotations for this experiment
|
|
await db.delete(annotation).where(eq(annotation.experimentId, id));
|
|
|
|
// Delete all participants for this experiment
|
|
await db.delete(participant).where(eq(participant.experimentId, id));
|
|
|
|
revalidatePath(`/admin/experiments/${id}`);
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error clearing experiment data:', error);
|
|
throw new Error('Failed to clear experiment data');
|
|
}
|
|
}
|
|
|
|
export async function getExperimentStatistics(id: number) {
|
|
const result = await requireAdmin();
|
|
if (!result.authenticated || !result.admin) {
|
|
throw new Error('Unauthorized');
|
|
}
|
|
|
|
try {
|
|
// Get the experiment with dataset
|
|
const exp = await db
|
|
.select()
|
|
.from(experiment)
|
|
.where(eq(experiment.id, id))
|
|
.limit(1);
|
|
|
|
if (!exp || exp.length === 0) {
|
|
throw new Error('Experiment not found');
|
|
}
|
|
|
|
const datasetId = exp[0].datasetId;
|
|
|
|
// Get all participants
|
|
const allParticipants = await db
|
|
.select()
|
|
.from(participant)
|
|
.where(eq(participant.experimentId, id));
|
|
|
|
// Count onboarding completion (simple check for non-null)
|
|
const completedOnboarding = allParticipants.filter(
|
|
(p) => p.onboardingAnswers !== null && p.onboardingAnswers !== undefined
|
|
).length;
|
|
|
|
// Check calibration completion using the proper validation
|
|
let completedCalibration = 0;
|
|
for (const p of allParticipants) {
|
|
if (await isParticipantCalibrationDone(id, p)) {
|
|
completedCalibration++;
|
|
}
|
|
}
|
|
|
|
// Count total dataset entries
|
|
const totalEntriesResult = await db
|
|
.select({ total: count().as('total') })
|
|
.from(dataset_entry)
|
|
.where(eq(dataset_entry.datasetId, datasetId));
|
|
|
|
const totalEntries = totalEntriesResult[0]?.total || 0;
|
|
|
|
// Get annotation statistics
|
|
const annotationStats = await db
|
|
.select({
|
|
total: count().as('total'),
|
|
entriesWithAnnotations: count(sql`DISTINCT ${annotation.datasetEntryId}`).as('entriesWithAnnotations'),
|
|
})
|
|
.from(annotation)
|
|
.where(eq(annotation.experimentId, id));
|
|
|
|
// Get per-entry annotation counts
|
|
const perEntryAnnotations = await db
|
|
.select({
|
|
datasetEntryId: annotation.datasetEntryId,
|
|
annotationCount: count().as('annotationCount'),
|
|
})
|
|
.from(annotation)
|
|
.where(eq(annotation.experimentId, id))
|
|
.groupBy(annotation.datasetEntryId);
|
|
|
|
// Calculate statistics
|
|
const totalAnnotations = annotationStats[0]?.total || 0;
|
|
const entriesWithAnnotations = annotationStats[0]?.entriesWithAnnotations || 0;
|
|
const entriesWithoutAnnotations = Math.max(0, totalEntries - entriesWithAnnotations);
|
|
const averageAnnotationsPerEntry = totalEntries > 0 ? (totalAnnotations / totalEntries).toFixed(2) : '0';
|
|
|
|
// Find max and min annotations per entry
|
|
let maxAnnotationsPerEntry = 0;
|
|
let minAnnotationsPerEntry = 0;
|
|
if (perEntryAnnotations.length > 0) {
|
|
const counts = perEntryAnnotations.map(p => p.annotationCount || 0);
|
|
maxAnnotationsPerEntry = Math.max(...counts);
|
|
minAnnotationsPerEntry = Math.min(...counts);
|
|
}
|
|
|
|
return {
|
|
participants: {
|
|
total: allParticipants.length,
|
|
completedOnboarding,
|
|
completedCalibration,
|
|
},
|
|
entries: {
|
|
total: totalEntries,
|
|
},
|
|
annotations: {
|
|
total: totalAnnotations,
|
|
entriesWithAnnotations,
|
|
entriesWithoutAnnotations,
|
|
averagePerEntry: parseFloat(averageAnnotationsPerEntry),
|
|
maxPerEntry: maxAnnotationsPerEntry,
|
|
minPerEntry: minAnnotationsPerEntry,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching experiment statistics:', error);
|
|
throw new Error('Failed to fetch experiment statistics');
|
|
}
|
|
}
|