'use client' import { useMemo, useState } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import type { SessionSummary } from '@/lib/parse-logs' import { formatCost, formatDuration, formatNumber } from '@/lib/format' const MODEL_COLORS: Record = { 'claude-opus-4-6': 'oklch(0.65 0.12 250)', 'claude-sonnet-4-6': 'oklch(0.70 0.12 200)', 'claude-haiku-4-5': 'oklch(0.75 0.10 155)', 'default': 'oklch(0.70 0.08 200)', } function getModelColor(model: string): string { for (const [key, color] of Object.entries(MODEL_COLORS)) { if (key !== 'default' && model.includes(key.replace('claude-', ''))) return color } return MODEL_COLORS.default } interface DayRow { date: string sessions: SessionSummary[] earliest: number // ms timestamp latest: number } export function SessionTimeline({ sessions }: { sessions: SessionSummary[] }) { const [hoveredSession, setHoveredSession] = useState(null) const { days, dayStart, dayEnd } = useMemo(() => { const dayMap = new Map() for (const s of sessions) { if (!s.startTime) continue const date = s.startTime.slice(0, 10) if (!dayMap.has(date)) dayMap.set(date, []) dayMap.get(date)!.push(s) } const rows: DayRow[] = [] for (const [date, daySessions] of dayMap) { const times = daySessions.flatMap(s => { const start = new Date(s.startTime).getTime() const end = s.endTime ? new Date(s.endTime).getTime() : start + s.durationMinutes * 60000 return [start, end] }) rows.push({ date, sessions: daySessions.sort((a, b) => a.startTime.localeCompare(b.startTime)), earliest: Math.min(...times), latest: Math.max(...times), }) } rows.sort((a, b) => b.date.localeCompare(a.date)) // Use 0:00 - 24:00 as the timeline range return { days: rows.slice(0, 30), // last 30 days dayStart: 0, dayEnd: 24 * 60, // in minutes } }, [sessions]) if (days.length === 0) { return ( Session Timeline No sessions with timestamp data ) } const totalMinutes = dayEnd - dayStart const hourMarkers = Array.from({ length: 25 }, (_, i) => i) return ( Session Timeline Daily session activity — each bar is a session, width = duration, color = model. Showing last {days.length} active days.
{/* Hour labels */}
{hourMarkers.filter(h => h % 3 === 0).map(h => ( {h === 0 ? '12a' : h < 12 ? `${h}a` : h === 12 ? '12p' : `${h - 12}p`} ))}
{/* Rows */}
{days.map(day => (
{/* Date label */}
{day.date.slice(5)}
{/* Timeline bar */}
{/* Hour gridlines */} {hourMarkers.filter(h => h % 6 === 0).map(h => (
))} {/* Session blocks */} {day.sessions.map(s => { const startDate = new Date(s.startTime) const startMin = startDate.getHours() * 60 + startDate.getMinutes() const duration = Math.max(s.durationMinutes, 3) // min 3min width for visibility const leftPct = (startMin / totalMinutes) * 100 const widthPct = Math.min((duration / totalMinutes) * 100, 100 - leftPct) const isHovered = hoveredSession === s.sessionId return ( setHoveredSession(s.sessionId)} onMouseLeave={() => setHoveredSession(null)} /> } />
{s.project}
{s.model}
{formatDuration(s.durationMinutes)} | {formatNumber(s.totalMessages)} msgs | {formatCost(s.costUSD)}
{new Date(s.startTime).toLocaleTimeString()} – {s.endTime ? new Date(s.endTime).toLocaleTimeString() : '?'}
) })}
))}
{/* Legend */}
{Object.entries(MODEL_COLORS).filter(([k]) => k !== 'default').map(([model, color]) => ( {model.replace('claude-', '')} ))}
) }