import { useMemo, useEffect, useRef } from "react"; import { ObjectLifecycleSequence } from "@/types/timeline"; import { LifecycleIcon } from "@/components/overlay/detail/ObjectLifecycle"; import { getLifecycleItemDescription } from "@/utils/lifecycleUtil"; import { useActivityStream } from "@/contexts/ActivityStreamContext"; type ActivityStreamProps = { timelineData: ObjectLifecycleSequence[]; currentTime: number; onSeek: (timestamp: number) => void; }; export default function ActivityStream({ timelineData, currentTime, onSeek, }: ActivityStreamProps) { const { selectedObjectId, annotationOffset } = useActivityStream(); const scrollRef = useRef(null); const effectiveTime = currentTime + annotationOffset; // group activities by timestamp (within 1 second resolution window) const groupedActivities = useMemo(() => { const groups: { [key: number]: ObjectLifecycleSequence[] } = {}; timelineData.forEach((activity) => { const groupKey = Math.floor(activity.timestamp); if (!groups[groupKey]) { groups[groupKey] = []; } groups[groupKey].push(activity); }); return Object.entries(groups) .map(([_timestamp, activities]) => { const sortedActivities = activities.sort( (a, b) => a.timestamp - b.timestamp, ); return { timestamp: sortedActivities[0].timestamp, // Original timestamp for display effectiveTimestamp: sortedActivities[0].timestamp + annotationOffset, // Adjusted for sorting/comparison activities: sortedActivities, }; }) .sort((a, b) => a.timestamp - b.timestamp); }, [timelineData, annotationOffset]); // Filter activities if object is selected const filteredGroups = useMemo(() => { if (!selectedObjectId) { return groupedActivities; } return groupedActivities .map((group) => ({ ...group, activities: group.activities.filter( (activity) => activity.source_id === selectedObjectId, ), })) .filter((group) => group.activities.length > 0); }, [groupedActivities, selectedObjectId]); // Auto-scroll to current time useEffect(() => { if (!scrollRef.current) return; // Find the last group where effectiveTimestamp <= currentTime + annotationOffset let currentGroupIndex = -1; for (let i = filteredGroups.length - 1; i >= 0; i--) { if (filteredGroups[i].effectiveTimestamp <= effectiveTime) { currentGroupIndex = i; break; } } if (currentGroupIndex !== -1) { const element = scrollRef.current.children[ currentGroupIndex ] as HTMLElement; if (element) { element.scrollIntoView({ behavior: "smooth", block: "center", }); } } }, [filteredGroups, effectiveTime, annotationOffset]); return (
{filteredGroups.length === 0 ? (
No activities found
) : ( filteredGroups.map((group) => ( )) )}
); } type ActivityGroupProps = { group: { timestamp: number; effectiveTimestamp: number; activities: ObjectLifecycleSequence[]; }; isCurrent: boolean; onSeek: (timestamp: number) => void; }; function ActivityGroup({ group, isCurrent, onSeek }: ActivityGroupProps) { const shouldExpand = group.activities.length > 1; return (
onSeek(group.timestamp)} >
{new Date(group.timestamp * 1000).toLocaleTimeString()}
{shouldExpand && (
{group.activities.length} activities
)}
{group.activities.map((activity, index) => ( ))}
); } type ActivityItemProps = { activity: ObjectLifecycleSequence; onSeek: (timestamp: number) => void; }; function ActivityItem({ activity }: ActivityItemProps) { const { selectedObjectId, setSelectedObjectId } = useActivityStream(); const handleObjectClick = (e: React.MouseEvent) => { e.stopPropagation(); if (selectedObjectId === activity.source_id) { setSelectedObjectId(undefined); } else { setSelectedObjectId(activity.source_id); } }; return (
{getLifecycleItemDescription(activity)}
{activity.source_id && ( )}
); }