"use client"; import React, { useMemo, useRef, useState } from "react"; import { usePlayback } from "./PlaybackContext"; import { cn } from "~/lib/utils"; import { AlertTriangle, CheckCircle, Flag, MessageSquare, Zap, Circle, Bot, User, Activity } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/components/ui/tooltip"; function formatTime(seconds: number) { const min = Math.floor(seconds / 60); const sec = Math.floor(seconds % 60); return `${min}:${sec.toString().padStart(2, "0")}`; } export function EventTimeline() { const { duration, currentTime, events, seekTo, startTime: contextStartTime } = usePlayback(); // Determine effective time range const sortedEvents = useMemo(() => { return [...events].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); }, [events]); const startTime = useMemo(() => { if (contextStartTime) return new Date(contextStartTime).getTime(); if (sortedEvents.length > 0) return new Date(sortedEvents[0]!.timestamp).getTime(); return 0; }, [contextStartTime, sortedEvents]); const effectiveDuration = useMemo(() => { if (duration > 0) return duration * 1000; if (sortedEvents.length === 0) return 60000; // 1 min default const end = new Date(sortedEvents[sortedEvents.length - 1]!.timestamp).getTime(); return Math.max(end - startTime, 1000); }, [duration, sortedEvents, startTime]); // Dimensions const containerRef = useRef(null); // Helpers const getPercentage = (timestampMs: number) => { const offset = timestampMs - startTime; return Math.max(0, Math.min(100, (offset / effectiveDuration) * 100)); }; const handleSeek = (e: React.MouseEvent) => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const pct = Math.max(0, Math.min(1, x / rect.width)); seekTo(pct * (effectiveDuration / 1000)); }; const currentProgress = (currentTime * 1000 / effectiveDuration) * 100; // Generate ticks for "number line" look // We want a major tick every ~10% or meaningful time interval const ticks = useMemo(() => { const count = 10; return Array.from({ length: count + 1 }).map((_, i) => ({ pct: (i / count) * 100, label: formatTime((effectiveDuration / 1000) * (i / count)) })); }, [effectiveDuration]); const getEventIcon = (type: string) => { if (type.includes("intervention") || type.includes("wizard")) return ; if (type.includes("robot") || type.includes("action")) return ; if (type.includes("completed")) return ; if (type.includes("start")) return ; if (type.includes("note")) return ; if (type.includes("error")) return ; return ; }; const getEventColor = (type: string) => { if (type.includes("intervention") || type.includes("wizard")) return "text-orange-500 border-orange-200 bg-orange-50"; if (type.includes("robot") || type.includes("action")) return "text-purple-500 border-purple-200 bg-purple-50"; if (type.includes("completed")) return "text-green-500 border-green-200 bg-green-50"; if (type.includes("start")) return "text-blue-500 border-blue-200 bg-blue-50"; if (type.includes("error")) return "text-red-500 border-red-200 bg-red-50"; return "text-slate-500 border-slate-200 bg-slate-50"; }; return (
{/* Timeline Track Container */}
{/* Background Grid/Ticks */}
{/* Major Ticks */} {ticks.map((tick, i) => (
{tick.label}
))}
{/* Central Axis Line */}
{/* Progress Fill (Subtle) */}
{/* Playhead */}
{/* Events "Lollipops" */} {sortedEvents.map((event, i) => { const pct = getPercentage(new Date(event.timestamp).getTime()); const isTop = i % 2 === 0; // Stagger events top/bottom return (
{ e.stopPropagation(); seekTo((new Date(event.timestamp).getTime() - startTime) / 1000); }} > {/* The Stem */}
{/* The Node */}
{getEventIcon(event.eventType)}
{event.eventType.replace(/_/g, " ")}
{new Date(event.timestamp).toLocaleTimeString()}
{!!event.data && (
{JSON.stringify(event.data as object).slice(0, 100)}
)}
); })}
); }