Add Detail stream in History view (#20525)

* new type

* activity stream panel

* use context provider for activity stream

* new activity stream panel in history view

* overlay for object tracking details in history view

* use overlay in video player

* don't refetch timeline

* fix activity stream group from being highlighted prematurely

* use annotation offset

* fix scrolling and use custom hook for interaction

* memoize to prevent unnecessary renders

* i18n and timestamp formatting

* add annotation offset slider

* bg color

* add collapsible component

* refactor

* rename activity to detail

* fix merge conflicts

* i18n

* more i18n
This commit is contained in:
Josh Hawkins
2025-10-16 07:24:14 -06:00
committed by GitHub
parent 2e7a2fd780
commit b52044aecc
16 changed files with 1818 additions and 326 deletions
@@ -0,0 +1,95 @@
import { useCallback, useState } from "react";
import { Slider } from "@/components/ui/slider";
import { Button } from "@/components/ui/button";
import { useDetailStream } from "@/context/detail-stream-context";
import axios from "axios";
import { useSWRConfig } from "swr";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
type Props = {
className?: string;
};
export default function AnnotationOffsetSlider({ className }: Props) {
const { annotationOffset, setAnnotationOffset, camera } = useDetailStream();
const { mutate } = useSWRConfig();
const { t } = useTranslation(["views/explore"]);
const [isSaving, setIsSaving] = useState(false);
const handleChange = useCallback(
(values: number[]) => {
if (!values || values.length === 0) return;
const valueMs = values[0];
setAnnotationOffset(valueMs);
},
[setAnnotationOffset],
);
const reset = useCallback(() => {
setAnnotationOffset(0);
}, [setAnnotationOffset]);
const save = useCallback(async () => {
setIsSaving(true);
try {
// save value in milliseconds to config
await axios.put(
`config/set?cameras.${camera}.detect.annotation_offset=${annotationOffset}`,
{ requires_restart: 0 },
);
toast.success(
t("objectLifecycle.annotationSettings.offset.toast.success", {
camera,
}),
{ position: "top-center" },
);
// refresh config
await mutate("config");
} catch (e: unknown) {
const err = e as {
response?: { data?: { message?: string } };
message?: string;
};
const errorMessage =
err?.response?.data?.message || err?.message || "Unknown error";
toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), {
position: "top-center",
});
} finally {
setIsSaving(false);
}
}, [annotationOffset, camera, mutate, t]);
return (
<div
className={`absolute bottom-0 left-0 right-0 z-30 flex items-center gap-3 bg-background p-3 ${className ?? ""}`}
style={{ pointerEvents: "auto" }}
>
<div className="w-56 text-sm">
Annotation offset (ms): {annotationOffset}
</div>
<div className="flex-1">
<Slider
value={[annotationOffset]}
min={-1500}
max={1500}
step={50}
onValueChange={handleChange}
/>
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="ghost" onClick={reset}>
Reset
</Button>
<Button size="sm" onClick={save} disabled={isSaving}>
{isSaving
? t("button.saving", { ns: "common" })
: t("button.save", { ns: "common" })}
</Button>
</div>
</div>
);
}
@@ -174,7 +174,7 @@ export function AnnotationSettingsPane({
{t("objectLifecycle.annotationSettings.offset.label")}
</FormLabel>
<div className="flex flex-col gap-3 md:flex-row-reverse md:gap-8">
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-0 md:my-5">
<div className="flex flex-row items-center gap-3 rounded-lg bg-destructive/50 p-3 text-sm text-primary-variant md:my-5">
<PiWarningCircle className="size-24" />
<div>
<Trans ns="views/explore">