"use client"; import { formatDistanceToNow } from "date-fns"; import { Calendar, Clock, Edit, Play, Settings, Users } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; import { useEffect, useState } from "react"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; import { EntityView, EntityViewHeader, EntityViewSection, EmptyState, InfoGrid, QuickActions, StatsGrid, } from "~/components/ui/entity-view"; import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider"; import { api } from "~/trpc/react"; import { useSession } from "next-auth/react"; interface ExperimentDetailPageProps { params: Promise<{ id: string }>; } const statusConfig = { draft: { label: "Draft", variant: "secondary" as const, icon: "FileText" as const, }, testing: { label: "Testing", variant: "outline" as const, icon: "TestTube" as const, }, ready: { label: "Ready", variant: "default" as const, icon: "CheckCircle" as const, }, deprecated: { label: "Deprecated", variant: "destructive" as const, icon: "AlertTriangle" as const, }, }; type Experiment = { id: string; name: string; description: string | null; status: string; createdAt: Date; updatedAt: Date; study: { id: string; name: string }; robot: { id: string; name: string; description: string | null } | null; protocol?: { blocks: unknown[] } | null; visualDesign?: unknown; studyId: string; createdBy: string; robotId: string | null; version: number; }; type Trial = { id: string; status: string; createdAt: Date; duration: number | null; participant: { id: string; participantCode: string; name?: string | null; } | null; experiment: { name: string } | null; participantId: string | null; experimentId: string; startedAt: Date | null; completedAt: Date | null; notes: string | null; updatedAt: Date; canAccess: boolean; userRole: string; }; export default function ExperimentDetailPage({ params, }: ExperimentDetailPageProps) { const { data: session } = useSession(); const [experiment, setExperiment] = useState(null); const [trials, setTrials] = useState([]); const [loading, setLoading] = useState(true); const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>( null, ); useEffect(() => { const resolveParams = async () => { const resolved = await params; setResolvedParams(resolved); }; void resolveParams(); }, [params]); const experimentQuery = api.experiments.get.useQuery( { id: resolvedParams?.id ?? "" }, { enabled: !!resolvedParams?.id }, ); const trialsQuery = api.trials.list.useQuery( { experimentId: resolvedParams?.id ?? "" }, { enabled: !!resolvedParams?.id }, ); useEffect(() => { if (experimentQuery.data) { setExperiment(experimentQuery.data); } }, [experimentQuery.data]); useEffect(() => { if (trialsQuery.data) { setTrials(trialsQuery.data); } }, [trialsQuery.data]); useEffect(() => { if (experimentQuery.isLoading || trialsQuery.isLoading) { setLoading(true); } else { setLoading(false); } }, [experimentQuery.isLoading, trialsQuery.isLoading]); // Set breadcrumbs useBreadcrumbsEffect([ { label: "Dashboard", href: "/", }, { label: "Studies", href: "/studies", }, { label: experiment?.study?.name ?? "Unknown Study", href: `/studies/${experiment?.study?.id}`, }, { label: "Experiments", href: `/studies/${experiment?.study?.id}/experiments`, }, { label: experiment?.name ?? "Experiment", }, ]); if (loading) return
Loading...
; if (experimentQuery.error) return notFound(); if (!experiment) return notFound(); const displayName = experiment.name ?? "Untitled Experiment"; const description = experiment.description; // Check if user can edit this experiment const userRoles = session?.user?.roles?.map((r) => r.role) ?? []; const canEdit = userRoles.includes("administrator") || userRoles.includes("researcher"); const statusInfo = statusConfig[experiment.status as keyof typeof statusConfig]; return ( ) : undefined } />
{/* Basic Information */} {experiment.study.name} ) : ( "No study assigned" ), }, { label: "Status", value: statusInfo?.label ?? "Unknown", }, { label: "Created", value: formatDistanceToNow(experiment.createdAt, { addSuffix: true, }), }, { label: "Last Updated", value: formatDistanceToNow(experiment.updatedAt, { addSuffix: true, }), }, ]} /> {/* Protocol Section */} Edit Protocol ) } > {experiment.protocol && typeof experiment.protocol === "object" && experiment.protocol !== null ? (
Protocol contains{" "} {Array.isArray( (experiment.protocol as { blocks: unknown[] }).blocks, ) ? (experiment.protocol as { blocks: unknown[] }).blocks .length : 0}{" "} blocks
) : ( Open Designer ) } /> )}
{/* Recent Trials */} View All } > {trials.length > 0 ? (
{trials.slice(0, 5).map((trial) => (
Trial #{trial.id.slice(-6)} {trial.status.charAt(0).toUpperCase() + trial.status.slice(1).replace("_", " ")}
{formatDistanceToNow(trial.createdAt, { addSuffix: true, })} {trial.duration && ( {Math.round(trial.duration / 60)} min )} {trial.participant && ( {trial.participant.name ?? trial.participant.participantCode} )}
))}
) : ( Start Trial ) } /> )}
{/* Statistics */} t.status === "completed").length, }, { label: "In Progress", value: trials.filter((t) => t.status === "in_progress") .length, }, ]} /> {/* Robot Information */} {experiment.robot && ( )} {/* Quick Actions */}
); }