import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { api, type SessionEventData } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { RatioBar, formatDuration, COLORS } from "@/components/analytics";
import { ArrowLeft, ChevronDown, ChevronRight, Zap, Clock, Layers } from "lucide-react";
export const Route = createFileRoute("/sessions_/$dbHash/$sessionId")({ component: EventView });
function EventRow({ e }: { e: { id: number; type: string; priority: number; data: string | null; created_at: string | null } }) {
const [expanded, setExpanded] = useState(false);
const raw = e.data || "-";
const truncated = raw.length > 100 ? raw.slice(0, 100) + "..." : raw;
const needsExpand = raw.length > 100;
return (
needsExpand && setExpanded(prev => !prev)}
>
{e.created_at || "-"}
= 3 ? "default" : "secondary"} className="text-[10px] shrink-0 h-5">
{e.type}
= 4 ? "text-red-500 font-semibold" : e.priority >= 3 ? "text-amber-500" : "text-muted-foreground"
}`}>
{expanded ? raw : truncated}
{needsExpand && (
{expanded ? : }
)}
);
}
function EventView() {
const { dbHash, sessionId } = Route.useParams();
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.events(dbHash, sessionId).then(d => { setData(d); setLoading(false); });
}, [dbHash, sessionId]);
if (loading || !data) return Loading session...
;
// Compute type counts
const typeCounts: Record = {};
data.events.forEach(e => { typeCounts[e.type] = (typeCounts[e.type] || 0) + 1; });
const typeEntries = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
const uniqueTypes = typeEntries.length;
// Compute duration from first to last event
let durationMin = 0;
if (data.events.length >= 2) {
const first = data.events[0]?.created_at;
const last = data.events[data.events.length - 1]?.created_at;
if (first && last) {
const start = new Date(first).getTime();
const end = new Date(last).getTime();
if (!isNaN(start) && !isNaN(end) && end > start) {
durationMin = (end - start) / 60000;
}
}
}
return (
Back to Sessions
Session Detail
{data.resume && Resume available}
{/* KPI Strip */}
Events
{data.events.length}
total in session
Duration
{formatDuration(durationMin)}
first to last event
Types
{uniqueTypes}
unique event types
{/* Event type breakdown as ratio bar */}
{typeEntries.length > 0 && (
Event Breakdown
({
label: type, value: count, color: COLORS[i % COLORS.length],
}))} />
)}
{/* Event type badges */}
{typeEntries.map(([type, count]) => (
{type}: {count}
))}
{/* Event list */}
{data.events.map(e => (
))}
{data.resume?.snapshot && (
Resume Snapshot ({data.resume.event_count} events, {data.resume.consumed ? "consumed" : "pending"})
{data.resume.snapshot}
)}
);
}