feat: added some additional data on export
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user