'use client'; import { format } from 'date-fns'; import { ChevronDown, LoaderCircle, X } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { useTaskTimelineQuery } from '@/hooks/use-api-queries'; import { buildStageTimeline, stageLabel, taskTypeLabel } from '@/components/notifications/task-stage-helpers'; import { StatusPill } from '@/components/ui/status-pill'; import { Button } from '@/components/ui/button'; import type { Task, TaskStageContext } from '@/lib/types'; function formatTimestamp(value: string | null) { if (!value) { return 'n/a'; } const parsed = new Date(value); if (Number.isNaN(parsed.getTime())) { return 'n/a'; } return format(parsed, 'MMM dd, yyyy HH:mm:ss'); } function formatCounterLabel(value: string) { return value .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/_/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()); } function ProgressPanel({ task }: { task: Task }) { const progress = task.notification.progress; if (!progress) { return null; } return (
{progress.current}/{progress.total} {progress.unit} {progress.percent ?? 0}%
); } function StatsPanel({ task }: { task: Task }) { const counters = task.stage_context?.counters ?? {}; const counterEntries = Object.entries(counters); if (task.notification.stats.length === 0 && counterEntries.length === 0) { return

No structured metrics available for this job yet.

; } return (
{task.notification.stats.map((stat) => (
{stat.label} {stat.value}
))} {counterEntries.map(([label, value]) => (
{formatCounterLabel(label)} {value}
))}
); } function StageContextBlock({ context }: { context: TaskStageContext | null }) { if (!context) { return null; } const counters = Object.entries(context.counters ?? {}); return (
{context.progress ? (
Progress {context.progress.current}/{context.progress.total} {context.progress.unit}
) : null} {context.subject ? (
{[context.subject.ticker, context.subject.accessionNumber, context.subject.label].filter(Boolean).join(' ยท ')}
) : null} {counters.length > 0 ? (
{counters.map(([label, value]) => ( {formatCounterLabel(label)}: {value} ))}
) : null}
); } type TaskDetailModalProps = { isOpen: boolean; taskId: string | null; onClose: () => void; }; export function TaskDetailModal({ isOpen, taskId, onClose }: TaskDetailModalProps) { const { data, isLoading, error } = useTaskTimelineQuery(taskId ?? '', isOpen && Boolean(taskId)); const task = data?.task ?? null; const events = data?.events ?? []; const timeline = task ? buildStageTimeline(task, events) : []; const [expandedStage, setExpandedStage] = useState(null); const defaultExpandedStage = useMemo(() => { if (task?.status === 'completed') { return null; } if (task?.status === 'failed') { return task.stage; } for (const item of timeline) { if (item.state === 'active') { return item.stage; } } for (let index = timeline.length - 1; index >= 0; index -= 1) { if (timeline[index]?.state === 'completed') { return timeline[index].stage; } } return timeline[0]?.stage ?? null; }, [timeline]); useEffect(() => { if (!isOpen) { setExpandedStage(null); return; } setExpandedStage((current) => { if (current && timeline.some((item) => item.stage === current)) { return current; } return defaultExpandedStage; }); }, [defaultExpandedStage, isOpen, timeline]); if (!isOpen || !taskId) { return null; } return (
{isLoading ? (
Loading task timeline...
{Array.from({ length: 4 }).map((_, index) => (
))}
) : null} {error ? (

Unable to load task timeline.

) : null} {task ? ( <>

Stage: {stageLabel(task.stage)}

Workflow run: {task.workflow_run_id ?? 'n/a'}

Created: {formatTimestamp(task.created_at)}

Finished: {formatTimestamp(task.finished_at)}

Updated: {formatTimestamp(task.updated_at)}

Attempts: {task.attempts}/{task.max_attempts}

Summary

{task.notification.title}

{task.notification.statusLine}

{task.notification.detailLine ? (

{task.notification.detailLine}

) : null}
{task.notification.actions .filter((action) => action.id !== 'open_details' && action.href) .map((action) => ( {action.label} ))}

Key metrics

Stage timeline

    {timeline.map((item) => (
  1. {expandedStage === item.stage ? (

    {item.detail ?? 'No additional detail for this step.'}

    ) : null}
  2. ))}
{task.error ? (

Error

{task.error}

) : null} {task.result ? (
Debug result
{JSON.stringify(task.result, null, 2)}
) : null} ) : null}
); }