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 } = useActivityStream(); const scrollRef = useRef(null); // 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]) => ({ timestamp: parseInt(timestamp), activities: activities.sort((a, b) => a.timestamp - b.timestamp), })) .sort((a, b) => a.timestamp - b.timestamp); }, [timelineData]); // 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; const currentGroupIndex = filteredGroups.findIndex( (group) => group.timestamp >= currentTime, ); if (currentGroupIndex !== -1) { const element = scrollRef.current.children[ currentGroupIndex ] as HTMLElement; if (element) { element.scrollIntoView({ behavior: "smooth", block: "center", }); } } }, [currentTime, filteredGroups]); return (
{filteredGroups.length === 0 ? (
No activities found
) : ( filteredGroups.map((group) => ( )) )}
); } type ActivityGroupProps = { group: { timestamp: 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 && ( )}
); }