import { useState, useMemo } from "react"; import type { ActivityLogEntry, ActivityEntryType } from "../types"; interface TimelineTabProps { entries: ActivityLogEntry[]; onArtifactClick: (datasetName: string) => void; } // Deterministic color hash for run-scoped labels function scopeColor(scope: string): string { if (scope === "debug") return "bg-gray-700 text-gray-300"; if (scope === "cross-run") return "bg-purple-900/60 text-purple-300"; if (scope === "meta") return "bg-blue-900/60 text-blue-300"; // Deterministic hash for other scopes (run labels etc.) let hash = 0; for (let i = 0; i < scope.length; i++) { hash = (hash * 31 + scope.charCodeAt(i)) & 0xffff; } const palette = [ "bg-emerald-900/60 text-emerald-300", "bg-amber-900/60 text-amber-300", "bg-rose-900/60 text-rose-300", "bg-teal-900/60 text-teal-300", "bg-indigo-900/60 text-indigo-300", "bg-fuchsia-900/60 text-fuchsia-300", "bg-orange-900/60 text-orange-300", "bg-lime-900/60 text-lime-300", ]; return palette[hash % palette.length]; } const TYPE_ICONS: Record = { action: "▶", result: "◆", note: "✎", milestone: "⚑", }; const TYPE_LABELS: ActivityEntryType[] = ["action", "result", "note", "milestone"]; function relativeTime(iso: string): string { const now = Date.now(); const then = new Date(iso).getTime(); const diffMs = now - then; if (isNaN(diffMs)) return iso; const diffSec = Math.floor(diffMs / 1000); if (diffSec < 60) return `${diffSec}s ago`; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `${diffMin}m ago`; const diffHr = Math.floor(diffMin / 60); if (diffHr < 24) return `${diffHr}h ago`; const diffDays = Math.floor(diffHr / 24); return `${diffDays}d ago`; } export default function TimelineTab({ entries, onArtifactClick }: TimelineTabProps) { const [scopeFilter, setScopeFilter] = useState("all"); const [activeTypes, setActiveTypes] = useState>( new Set(TYPE_LABELS) ); // Sorted most-recent-first (ensure stable order even if backend varies) const sorted = useMemo( () => [...entries].sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime() ), [entries] ); const uniqueScopes = useMemo(() => { const set = new Set(); entries.forEach((e) => set.add(e.scope)); return Array.from(set).sort(); }, [entries]); const filtered = useMemo( () => sorted.filter( (e) => (scopeFilter === "all" || e.scope === scopeFilter) && activeTypes.has(e.type) ), [sorted, scopeFilter, activeTypes] ); function toggleType(t: ActivityEntryType) { setActiveTypes((prev) => { const next = new Set(prev); if (next.has(t)) { // Keep at least one type active if (next.size > 1) next.delete(t); } else { next.add(t); } return next; }); } if (entries.length === 0) { return (
No activity log entries yet.
); } return (
{/* Filter controls */}
{/* Scope dropdown */}
Scope
{/* Type filter chips */}
Type {TYPE_LABELS.map((t) => ( ))}
LLM-generated log {filtered.length} of {entries.length}
{/* Entries */} {filtered.length === 0 ? (
No entries match the current filters.
) : (
{filtered.map((entry, i) => (
{/* Timeline dot */}
{/* Timestamp */} {relativeTime(entry.timestamp)} {/* Scope badge */} {entry.scope} {/* Type icon + label */} {TYPE_ICONS[entry.type]} {/* Author */} {entry.author === "agent" ? "Claude Code" : "Researcher"}
{/* Message */}

{entry.message}

{/* Artifact chips */} {entry.artifacts && entry.artifacts.length > 0 && (
{entry.artifacts.map((ds) => ( ))}
)}
))}
)}
); }