From 5f3a51f002764dd66c84990ba200631cd98ac886 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Fri, 6 Mar 2026 21:07:51 -0600
Subject: [PATCH] improve detail stream ux
---
.../overlay/detail/AnnotationOffsetSlider.tsx | 63 +++++++--------
web/src/components/timeline/DetailStream.tsx | 81 +++++++++++++++++--
2 files changed, 103 insertions(+), 41 deletions(-)
diff --git a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx
index d74909e86..665a04553 100644
--- a/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx
+++ b/web/src/components/overlay/detail/AnnotationOffsetSlider.tsx
@@ -137,42 +137,35 @@ export default function AnnotationOffsetSlider({ className }: Props) {
-
-
-
- trackingDetails.annotationSettings.offset.millisecondsToOffset
-
-
-
-
-
-
- {t("trackingDetails.annotationSettings.offset.tips")}
-
-
-
-
-
);
diff --git a/web/src/components/timeline/DetailStream.tsx b/web/src/components/timeline/DetailStream.tsx
index ef9cd6364..baa665f50 100644
--- a/web/src/components/timeline/DetailStream.tsx
+++ b/web/src/components/timeline/DetailStream.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { TrackingDetailsSequence } from "@/types/timeline";
import { getLifecycleItemDescription } from "@/utils/lifecycleUtil";
import { useDetailStream } from "@/context/detail-stream-context";
@@ -33,6 +33,7 @@ import { MdAutoAwesome } from "react-icons/md";
import { isPWA } from "@/utils/isPWA";
import { isInIframe } from "@/utils/isIFrame";
import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip";
+import { Separator } from "../ui/separator";
type DetailStreamProps = {
reviewItems?: ReviewSegment[];
@@ -49,7 +50,8 @@ export default function DetailStream({
}: DetailStreamProps) {
const { data: config } = useSWR("config");
const { t } = useTranslation("views/events");
- const { annotationOffset } = useDetailStream();
+ const { annotationOffset, selectedObjectIds, setSelectedObjectIds } =
+ useDetailStream();
const scrollRef = useRef(null);
const [activeReviewId, setActiveReviewId] = useState(
@@ -67,9 +69,69 @@ export default function DetailStream({
true,
);
- const onSeekCheckPlaying = (timestamp: number) => {
- onSeek(timestamp, isPlaying);
- };
+ // When the settings panel opens, pin to the nearest review with detections
+ // so the user can visually align the bounding box using the offset slider
+ const pinnedDetectTimestampRef = useRef(null);
+ const wasControlsExpandedRef = useRef(false);
+ const selectedBeforeExpandRef = useRef([]);
+
+ const onSeekCheckPlaying = useCallback(
+ (timestamp: number) => {
+ onSeek(timestamp, isPlaying);
+ },
+ [onSeek, isPlaying],
+ );
+
+ useEffect(() => {
+ if (controlsExpanded && !wasControlsExpandedRef.current) {
+ selectedBeforeExpandRef.current = selectedObjectIds;
+
+ const items = (reviewItems ?? []).filter(
+ (r) => r.data?.detections?.length > 0,
+ );
+ if (items.length > 0) {
+ // Pick the nearest review to current effective time
+ let nearest = items[0];
+ let minDiff = Math.abs(effectiveTime - nearest.start_time);
+ for (const r of items) {
+ const diff = Math.abs(effectiveTime - r.start_time);
+ if (diff < minDiff) {
+ nearest = r;
+ minDiff = diff;
+ }
+ }
+
+ const nearestId = `review-${nearest.id ?? nearest.start_time ?? Math.floor(nearest.start_time ?? 0)}`;
+ setActiveReviewId(nearestId);
+
+ const detectionId = nearest.data.detections[0];
+ setSelectedObjectIds([detectionId]);
+
+ // Use the detection's actual start timestamp (parsed from its ID)
+ // rather than review.start_time, which can be >10ms away from any
+ // lifecycle event and would fail the bounding-box TOLERANCE check.
+ const detectTimestamp = parseFloat(detectionId);
+ pinnedDetectTimestampRef.current = detectTimestamp;
+ const recordTime = detectTimestamp + annotationOffset / 1000;
+ onSeek(recordTime, false);
+ }
+ }
+ if (!controlsExpanded && wasControlsExpandedRef.current) {
+ pinnedDetectTimestampRef.current = null;
+ setSelectedObjectIds(selectedBeforeExpandRef.current);
+ }
+ wasControlsExpandedRef.current = controlsExpanded;
+ // Only trigger on expand/collapse transition
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [controlsExpanded]);
+
+ // Re-seek on annotation offset change while settings panel is open
+ useEffect(() => {
+ const pinned = pinnedDetectTimestampRef.current;
+ if (!controlsExpanded || pinned == null) return;
+ const recordTime = pinned + annotationOffset / 1000;
+ onSeek(recordTime, false);
+ }, [controlsExpanded, annotationOffset, onSeek]);
// Ensure we initialize the active review when reviewItems first arrive.
// This helps when the component mounts while the video is already
@@ -214,6 +276,12 @@ export default function DetailStream({
/>
+ {controlsExpanded && (
+
setControlsExpanded(false)}
+ />
+ )}