diff --git a/web/src/components/timeline/EventReviewTimeline.tsx b/web/src/components/timeline/EventReviewTimeline.tsx index 4b6735093..73d5fae10 100644 --- a/web/src/components/timeline/EventReviewTimeline.tsx +++ b/web/src/components/timeline/EventReviewTimeline.tsx @@ -15,7 +15,7 @@ export type EventReviewTimelineProps = { segmentDuration: number; timestampSpread: number; timelineStart: number; - timelineDuration?: number; + timelineEnd: number; showHandlebar?: boolean; handlebarTime?: number; showMinimap?: boolean; @@ -30,7 +30,7 @@ export function EventReviewTimeline({ segmentDuration, timestampSpread, timelineStart, - timelineDuration = 24 * 60 * 60, + timelineEnd, showHandlebar = false, handlebarTime, showMinimap = false, @@ -46,6 +46,10 @@ export function EventReviewTimeline({ const timelineRef = useRef(null); const currentTimeRef = useRef(null); const observer = useRef(null); + const timelineDuration = useMemo( + () => timelineEnd - timelineStart, + [timelineEnd, timelineStart] + ); const { alignDateToTimeline } = useEventUtils(events, segmentDuration); diff --git a/web/src/components/timeline/EventSegment.tsx b/web/src/components/timeline/EventSegment.tsx index 3650b5922..8d3507079 100644 --- a/web/src/components/timeline/EventSegment.tsx +++ b/web/src/components/timeline/EventSegment.tsx @@ -1,7 +1,7 @@ import { useEventUtils } from "@/hooks/use-event-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; -import { useMemo } from "react"; +import React, { useMemo } from "react"; type EventSegmentProps = { events: ReviewSegment[]; @@ -45,7 +45,7 @@ function MinimapBounds({ return ( <> {isFirstSegmentInMinimap && ( -
+
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", @@ -56,7 +56,7 @@ function MinimapBounds({ )} {isLastSegmentInMinimap && ( -
+
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", @@ -76,14 +76,14 @@ function Tick({ timestampSpread, }: TickSegmentProps) { return ( -
+
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
)} @@ -99,7 +99,7 @@ function Timestamp({ segmentKey, }: TimestampSegmentProps) { return ( -
+
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
getSeverity(segmentTime), + () => getSeverity(segmentTime, displaySeverityType), [getSeverity, segmentTime] ); const reviewed = useMemo( @@ -195,13 +195,13 @@ export function EventSegment({ const severityColors: { [key: number]: string } = { 1: reviewed - ? "from-severity_motion-dimmed/30 to-severity_motion/30" + ? "from-severity_motion-dimmed/50 to-severity_motion/50" : "from-severity_motion-dimmed to-severity_motion", 2: reviewed - ? "from-severity_detection-dimmed/30 to-severity_detection/30" + ? "from-severity_detection-dimmed/50 to-severity_detection/50" : "from-severity_detection-dimmed to-severity_detection", 3: reviewed - ? "from-severity_alert-dimmed/30 to-severity_alert/30" + ? "from-severity_alert-dimmed/50 to-severity_alert/50" : "from-severity_alert-dimmed to-severity_alert", }; @@ -229,35 +229,40 @@ export function EventSegment({ segmentKey={segmentKey} /> - {severity == displaySeverityType && ( -
-
( + + {severityValue === displaySeverityType && ( +
+
-
- )} + >
+
+ )} - {severity != displaySeverityType && ( -
-
+
-
- )} + >
+
+ )} + + ))}
); } diff --git a/web/src/hooks/use-event-utils.ts b/web/src/hooks/use-event-utils.ts index d98ede6d8..b8483d8e6 100644 --- a/web/src/hooks/use-event-utils.ts +++ b/web/src/hooks/use-event-utils.ts @@ -1,37 +1,61 @@ -import { useCallback } from 'react'; -import { ReviewSegment } from '@/types/review'; +import { useCallback } from "react"; +import { ReviewSegment } from "@/types/review"; -export const useEventUtils = (events: ReviewSegment[], segmentDuration: number) => { - const isStartOfEvent = useCallback((time: number): boolean => { - return events.some((event) => { - const segmentStart = getSegmentStart(event.start_time); - return time >= segmentStart && time < segmentStart + segmentDuration; - }); - }, [events, segmentDuration]); +export const useEventUtils = ( + events: ReviewSegment[], + segmentDuration: number +) => { + const isStartOfEvent = useCallback( + (time: number): boolean => { + return events.some((event) => { + const segmentStart = getSegmentStart(event.start_time); + return time >= segmentStart && time < segmentStart + segmentDuration; + }); + }, + [events, segmentDuration] + ); - const isEndOfEvent = useCallback((time: number): boolean => { - return events.some((event) => { - if (typeof event.end_time === 'number') { - const segmentEnd = getSegmentEnd(event.end_time); - return time >= segmentEnd - segmentDuration && time < segmentEnd; - } - return false; // Return false if end_time is undefined - }); - }, [events, segmentDuration]); + const isEndOfEvent = useCallback( + (time: number): boolean => { + return events.some((event) => { + if (typeof event.end_time === "number") { + const segmentEnd = getSegmentEnd(event.end_time); + return time >= segmentEnd - segmentDuration && time < segmentEnd; + } + return false; + }); + }, + [events, segmentDuration] + ); - const getSegmentStart = useCallback((time: number): number => { - return Math.floor(time / (segmentDuration)) * (segmentDuration); - }, [segmentDuration]); + const getSegmentStart = useCallback( + (time: number): number => { + return Math.floor(time / segmentDuration) * segmentDuration; + }, + [segmentDuration] + ); - const getSegmentEnd = useCallback((time: number): number => { - return Math.ceil(time / (segmentDuration)) * (segmentDuration); - }, [segmentDuration]); + const getSegmentEnd = useCallback( + (time: number): number => { + return Math.ceil(time / segmentDuration) * segmentDuration; + }, + [segmentDuration] + ); - const alignDateToTimeline = useCallback((time: number): number => { - const remainder = time % (segmentDuration); - const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; - return time + adjustment; - }, [segmentDuration]); + const alignDateToTimeline = useCallback( + (time: number): number => { + const remainder = time % segmentDuration; + const adjustment = remainder !== 0 ? segmentDuration - remainder : 0; + return time + adjustment; + }, + [segmentDuration] + ); - return { isStartOfEvent, isEndOfEvent, getSegmentStart, getSegmentEnd, alignDateToTimeline }; + return { + isStartOfEvent, + isEndOfEvent, + getSegmentStart, + getSegmentEnd, + alignDateToTimeline, + }; }; diff --git a/web/src/hooks/use-handle-dragging.ts b/web/src/hooks/use-handle-dragging.ts index 6d74384ca..c63d4e2d0 100644 --- a/web/src/hooks/use-handle-dragging.ts +++ b/web/src/hooks/use-handle-dragging.ts @@ -50,7 +50,11 @@ function useDraggableHandler({ const handleMouseMove = useCallback( (e: MouseEvent) => { - if (!contentRef.current || !timelineRef.current || !scrollTimeRef.current) { + if ( + !contentRef.current || + !timelineRef.current || + !scrollTimeRef.current + ) { return; } @@ -68,9 +72,7 @@ function useDraggableHandler({ const segmentHeight = timelineHeight / (timelineDuration / segmentDuration); - const getCumulativeScrollTop = ( - element: HTMLElement | null - ) => { + const getCumulativeScrollTop = (element: HTMLElement | null) => { let scrollTop = 0; while (element) { scrollTop += element.scrollTop; @@ -100,7 +102,7 @@ function useDraggableHandler({ thumb.style.top = `${newHandlePosition - segmentHeight}px`; if (currentTimeRef.current) { currentTimeRef.current.textContent = new Date( - segmentStartTime*1000 + segmentStartTime * 1000 ).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", diff --git a/web/src/hooks/use-segment-utils.ts b/web/src/hooks/use-segment-utils.ts index c31393a2e..c9d00002d 100644 --- a/web/src/hooks/use-segment-utils.ts +++ b/web/src/hooks/use-segment-utils.ts @@ -1,22 +1,30 @@ -import { useCallback, useMemo } from 'react'; -import { ReviewSegment } from '@/types/review'; +import { useCallback, useMemo } from "react"; +import { ReviewSegment } from "@/types/review"; export const useSegmentUtils = ( segmentDuration: number, events: ReviewSegment[], - severityType: string, + severityType: string ) => { - const getSegmentStart = useCallback((time: number): number => { - return Math.floor(time / (segmentDuration)) * (segmentDuration); - }, [segmentDuration]); + const getSegmentStart = useCallback( + (time: number): number => { + return Math.floor(time / segmentDuration) * segmentDuration; + }, + [segmentDuration] + ); - const getSegmentEnd = useCallback((time: number | undefined): number => { - if (time) { - return Math.ceil(time / (segmentDuration)) * (segmentDuration); - } else { - return (Date.now()/1000)+(segmentDuration); - } - }, [segmentDuration]); + const getSegmentEnd = useCallback( + (time: number | undefined): number => { + if (time) { + return ( + Math.floor(time / segmentDuration) * segmentDuration + segmentDuration + ); + } else { + return Date.now() / 1000 + segmentDuration; + } + }, + [segmentDuration] + ); const mapSeverityToNumber = useCallback((severity: string): number => { switch (severity) { @@ -36,75 +44,94 @@ export const useSegmentUtils = ( [severityType] ); - const getSeverity = useCallback((time: number): number => { - const activeEvents = events?.filter((event) => { - const segmentStart = getSegmentStart(event.start_time); - const segmentEnd = getSegmentEnd(event.end_time); - return time >= segmentStart && time < segmentEnd; - }); - if (activeEvents?.length === 0) return 0; // No event at this time - const severityValues = activeEvents?.map((event) => - mapSeverityToNumber(event.severity) - ); - return Math.max(...severityValues); - }, [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber]); + const getSeverity = useCallback( + (time: number, displaySeverityType: number): number[] => { + const activeEvents = events?.filter((event) => { + const segmentStart = getSegmentStart(event.start_time); + const segmentEnd = getSegmentEnd(event.end_time); + return time >= segmentStart && time < segmentEnd; + }); - const getReviewed = useCallback((time: number): boolean => { - return events.some((event) => { - const segmentStart = getSegmentStart(event.start_time); - const segmentEnd = getSegmentEnd(event.end_time); - return ( - time >= segmentStart && time < segmentEnd && event.has_been_reviewed + if (activeEvents?.length === 0) return [0]; + const severityValues = activeEvents.map((event) => + mapSeverityToNumber(event.severity) ); - }); - }, [events, getSegmentStart, getSegmentEnd]); + const highestSeverityValue = Math.max(...severityValues); + + if ( + severityValues.includes(displaySeverityType) && + displaySeverityType !== highestSeverityValue + ) { + return [displaySeverityType, highestSeverityValue]; + } else { + return [highestSeverityValue]; + } + }, + [events, getSegmentStart, getSegmentEnd, mapSeverityToNumber] + ); + + const getReviewed = useCallback( + (time: number): boolean => { + return events.some((event) => { + const segmentStart = getSegmentStart(event.start_time); + const segmentEnd = getSegmentEnd(event.end_time); + return ( + time >= segmentStart && time < segmentEnd && event.has_been_reviewed + ); + }); + }, + [events, getSegmentStart, getSegmentEnd] + ); const shouldShowRoundedCorners = useCallback( - (segmentTime: number): { roundTop: boolean, roundBottom: boolean } => { - + (segmentTime: number): { roundTop: boolean; roundBottom: boolean } => { const prevSegmentTime = segmentTime - segmentDuration; const nextSegmentTime = segmentTime + segmentDuration; - const severityEvents = events.filter(e => e.severity === severityType); + const severityEvents = events.filter((e) => e.severity === severityType); - const otherEvents = events.filter(e => e.severity !== severityType); + const otherEvents = events.filter((e) => e.severity !== severityType); - const hasPrevSeverityEvent = severityEvents.some(e => { + const hasPrevSeverityEvent = severityEvents.some((e) => { return ( prevSegmentTime >= getSegmentStart(e.start_time) && prevSegmentTime < getSegmentEnd(e.end_time) ); }); - const hasNextSeverityEvent = severityEvents.some(e => { + const hasNextSeverityEvent = severityEvents.some((e) => { return ( nextSegmentTime >= getSegmentStart(e.start_time) && nextSegmentTime < getSegmentEnd(e.end_time) ); }); - const hasPrevOtherEvent = otherEvents.some(e => { - return ( - prevSegmentTime >= getSegmentStart(e.start_time) && - prevSegmentTime < getSegmentEnd(e.end_time) - ); + const hasPrevOtherEvent = otherEvents.some((e) => { + return ( + prevSegmentTime >= getSegmentStart(e.start_time) && + prevSegmentTime < getSegmentEnd(e.end_time) + ); }); - const hasNextOtherEvent = otherEvents.some(e => { - return ( - nextSegmentTime >= getSegmentStart(e.start_time) && - nextSegmentTime < getSegmentEnd(e.end_time) - ); + const hasNextOtherEvent = otherEvents.some((e) => { + return ( + nextSegmentTime >= getSegmentStart(e.start_time) && + nextSegmentTime < getSegmentEnd(e.end_time) + ); }); - const hasOverlappingSeverityEvent = severityEvents.some(e => { - return segmentTime >= getSegmentStart(e.start_time) && - segmentTime < getSegmentEnd(e.end_time) + const hasOverlappingSeverityEvent = severityEvents.some((e) => { + return ( + segmentTime >= getSegmentStart(e.start_time) && + segmentTime < getSegmentEnd(e.end_time) + ); }); - const hasOverlappingOtherEvent = otherEvents.some(e => { - return segmentTime >= getSegmentStart(e.start_time) && - segmentTime < getSegmentEnd(e.end_time) + const hasOverlappingOtherEvent = otherEvents.some((e) => { + return ( + segmentTime >= getSegmentStart(e.start_time) && + segmentTime < getSegmentEnd(e.end_time) + ); }); let roundTop = false; @@ -123,12 +150,18 @@ export const useSegmentUtils = ( return { roundTop, - roundBottom + roundBottom, }; - }, [events, getSegmentStart, getSegmentEnd, segmentDuration, severityType] ); - return { getSegmentStart, getSegmentEnd, getSeverity, displaySeverityType, getReviewed, shouldShowRoundedCorners }; + return { + getSegmentStart, + getSegmentEnd, + getSeverity, + displaySeverityType, + getReviewed, + shouldShowRoundedCorners, + }; }; diff --git a/web/src/pages/UIPlayground.tsx b/web/src/pages/UIPlayground.tsx index 42b4e88f7..68c44ab14 100644 --- a/web/src/pages/UIPlayground.tsx +++ b/web/src/pages/UIPlayground.tsx @@ -188,7 +188,7 @@ function UIPlayground() { segmentDuration={60} // seconds per segment timestampSpread={15} // minutes between each major timestamp timelineStart={Math.floor(Date.now() / 1000)} // start of the timeline - all times are numeric, not Date objects - timelineDuration={24 * 60 * 60} // in minutes, defaults to 24 hours + timelineEnd={Math.floor(Date.now() / 1000) + 2 * 60 * 60} // end of timeline - timestamp showHandlebar // show / hide the handlebar handlebarTime={Math.floor(Date.now() / 1000) - 27 * 60} // set the time of the handlebar showMinimap // show / hide the minimap