feat: added some additional data on export

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
averel10
2026-04-30 15:19:04 +02:00
parent f0334f2580
commit f8cfd4b1e2
2 changed files with 65 additions and 25 deletions

View File

@@ -4,8 +4,9 @@ import db from '@/lib/db';
import { participant } from '@/lib/model/participant'; import { participant } from '@/lib/model/participant';
import { annotation } from '@/lib/model/annotation'; import { annotation } from '@/lib/model/annotation';
import { experiment } from '@/lib/model/experiment'; import { experiment } from '@/lib/model/experiment';
import { experiment_calibration, ExperimentCalibration } from '@/lib/model/experiment_calibration';
import { dataset } from '@/lib/model/dataset'; import { dataset } from '@/lib/model/dataset';
import { dataset_entry } from '@/lib/model/dataset_entry'; import { dataset_entry, DatasetEntry } from '@/lib/model/dataset_entry';
import { user } from '@/lib/model/auth-schema'; import { user } from '@/lib/model/auth-schema';
import { and, eq, count } from 'drizzle-orm'; import { and, eq, count } from 'drizzle-orm';
import { requireAdmin } from '@/lib/auth'; import { requireAdmin } from '@/lib/auth';
@@ -24,19 +25,23 @@ export interface ParticipantListItem {
updatedAt: Date; updatedAt: Date;
} }
export interface EnrichedCalibrationAnswer {
dialectLabel: string;
confidence: number;
calibrationEntry: ExperimentCalibration | null;
}
export interface ParticipantDetail extends ParticipantListItem { export interface ParticipantDetail extends ParticipantListItem {
onboardingAnswers: any; onboardingAnswers: any;
calibrationAnswers: any; calibrationAnswers: EnrichedCalibrationAnswer[];
annotations: Array<{ annotations: Array<{
id: number; id: number;
datasetEntryId: number;
externalId: string;
fileName: string;
datasetId: number;
rating: number; rating: number;
dialectLabel: string;
createdAt: Date;
confidence: number; confidence: number;
createdAt: Date;
datasetEntry: DatasetEntry | null;
}>; }>;
} }
@@ -166,7 +171,7 @@ export async function getParticipantDetail(
const participantAnnotations = await db const participantAnnotations = await db
.select({ .select({
id: annotation.id, id: annotation.id,
datasetEntryId: annotation.datasetEntryId, datasetEntry: dataset_entry,
externalId: dataset_entry.externalId, externalId: dataset_entry.externalId,
fileName: dataset_entry.fileName, fileName: dataset_entry.fileName,
datasetId: dataset_entry.datasetId, datasetId: dataset_entry.datasetId,
@@ -184,6 +189,42 @@ export async function getParticipantDetail(
) )
); );
// Enrich calibration answers with calibration entry details
const enrichedCalibrationAnswers: EnrichedCalibrationAnswer[] = [];
if (p.calibrationAnswers && typeof p.calibrationAnswers === 'object') {
const answers = p.calibrationAnswers as Record<number, any>;
// Get all calibration entries for this experiment
const calibrationEntries = await db
.select()
.from(experiment_calibration)
.where(eq(experiment_calibration.experimentId, experimentId));
// Create a map of calibration entries by ID for quick lookup
const entriesMap = new Map(calibrationEntries.map(e => [e.id, e]));
// Enrich each answer with calibration entry data
for (const [itemId, answer] of Object.entries(answers)) {
const calibrationId = parseInt(itemId);
const entry = entriesMap.get(calibrationId);
if (entry) {
enrichedCalibrationAnswers.push({
dialectLabel: answer.dialectLabel,
confidence: answer.confidence,
calibrationEntry: entry,
});
} else {
// Fallback if entry not found
enrichedCalibrationAnswers.push({
dialectLabel: answer.dialectLabel,
confidence: answer.confidence,
calibrationEntry: null,
});
}
}
}
// Use the proper calibration validation function // Use the proper calibration validation function
const completedCalibration = await isParticipantCalibrationDone(experimentId, p); const completedCalibration = await isParticipantCalibrationDone(experimentId, p);
@@ -195,16 +236,14 @@ export async function getParticipantDetail(
completedOnboarding: p.onboardingAnswers !== null && p.onboardingAnswers !== undefined, completedOnboarding: p.onboardingAnswers !== null && p.onboardingAnswers !== undefined,
completedCalibration, completedCalibration,
onboardingAnswers: p.onboardingAnswers || {}, onboardingAnswers: p.onboardingAnswers || {},
calibrationAnswers: p.calibrationAnswers || {}, calibrationAnswers: enrichedCalibrationAnswers,
annotationCount: participantAnnotations.length, annotationCount: participantAnnotations.length,
annotations: participantAnnotations.map(a => ({ annotations: participantAnnotations.map(a => ({
id: a.id, id: a.id,
datasetEntryId: a.datasetEntryId, rating: a.rating,
externalId: a.externalId || 'Unknown', fileName: a.fileName || 'Unknown',
datasetId: a.datasetId || 0, rating: a.rating,
dialectLabel: a.dialectLabel,
confidence: a.confidence, confidence: a.confidence,
createdAt: a.createdAt, createdAt: a.createdAt,
datasetEntry: a.datasetEntry
})), })),
createdAt: p.createdAt, createdAt: p.createdAt,
updatedAt: p.updatedAt, updatedAt: p.updatedAt,
@@ -311,18 +350,19 @@ export async function exportParticipantDataAsJson(
}, },
calibration: { calibration: {
completed: participantDetail.completedCalibration, completed: participantDetail.completedCalibration,
answers: participantDetail.calibrationAnswers, entries: participantDetail.calibrationAnswers.map((answer) => ({
dialectLabel: answer.dialectLabel,
confidence: answer.confidence,
calibrationEntry: answer.calibrationEntry
})),
scores: calibrationScores, scores: calibrationScores,
}, },
annotations: participantDetail.annotations.map((ann) => ({ annotations: participantDetail.annotations.map((ann) => ({
id: ann.id, id: ann.id,
externalId: ann.externalId,
fileName: ann.fileName,
datasetId: ann.datasetId,
rating: ann.rating, rating: ann.rating,
confidence: ann.confidence, confidence: ann.confidence,
dialectLabel: ann.dialectLabel,
createdAt: ann.createdAt, createdAt: ann.createdAt,
datasetEntry: ann.datasetEntry
})), })),
}; };

View File

@@ -127,10 +127,10 @@ export default function ParticipantDetailView({ participant, experimentId }: Par
: 'border-gray-200 bg-gray-50 hover:border-gray-300' : 'border-gray-200 bg-gray-50 hover:border-gray-300'
}`} }`}
> >
<div className="font-medium text-gray-900">{annotation.externalId}</div> <div className="font-medium text-gray-900">{annotation.datasetEntry?.externalId || 'Unknown'}</div>
<div className="text-sm text-gray-600 mt-1"> <div className="text-sm text-gray-600 mt-1">
Rating: <span className="font-semibold">{annotation.rating}</span> | Dialect:{' '} Rating: <span className="font-semibold">{annotation.rating}</span> | Dialect:{' '}
<span className="font-semibold">{annotation.dialectLabel}</span> <span className="font-semibold">{annotation.datasetEntry?.dialect || 'Unknown'}</span>
</div> </div>
<div className="text-xs text-gray-500 mt-1"> <div className="text-xs text-gray-500 mt-1">
{new Date(annotation.createdAt).toLocaleString()} {new Date(annotation.createdAt).toLocaleString()}
@@ -147,7 +147,7 @@ export default function ParticipantDetailView({ participant, experimentId }: Par
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<div className="text-sm text-gray-600 mb-1">External ID</div> <div className="text-sm text-gray-600 mb-1">External ID</div>
<div className="font-medium text-gray-900 break-all">{selectedAnnotation.externalId}</div> <div className="font-medium text-gray-900 break-all">{selectedAnnotation.datasetEntry?.externalId || 'Unknown'}</div>
</div> </div>
<div> <div>
<div className="text-sm text-gray-600 mb-1">Rating</div> <div className="text-sm text-gray-600 mb-1">Rating</div>
@@ -160,7 +160,7 @@ export default function ParticipantDetailView({ participant, experimentId }: Par
<div> <div>
<div className="text-sm text-gray-600 mb-1">Dialect</div> <div className="text-sm text-gray-600 mb-1">Dialect</div>
<div className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium"> <div className="inline-block px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
{selectedAnnotation.dialectLabel} {selectedAnnotation.datasetEntry?.dialect || 'Unknown'}
</div> </div>
</div> </div>
<div> <div>
@@ -170,8 +170,8 @@ export default function ParticipantDetailView({ participant, experimentId }: Par
<div> <div>
<div className="text-sm text-gray-600 mb-1">Audio</div> <div className="text-sm text-gray-600 mb-1">Audio</div>
{(() => { {(() => {
const fileExtension = selectedAnnotation.fileName.substring(selectedAnnotation.fileName.lastIndexOf('.')); const fileExtension = selectedAnnotation.datasetEntry?.fileName.substring(selectedAnnotation.datasetEntry?.fileName.lastIndexOf('.'));
const src = `/public/datasets/${selectedAnnotation.datasetId}/${selectedAnnotation.externalId}${fileExtension}`; const src = `/public/datasets/${selectedAnnotation.datasetEntry?.datasetId}/${selectedAnnotation.datasetEntry?.externalId}${fileExtension}`;
return <WaveformPlayer src={src} showWaveform={false} playMode='stop' />; return <WaveformPlayer src={src} showWaveform={false} playMode='stop' />;
})()} })()}
</div> </div>