@@ -581,10 +628,12 @@ export default function ObjectLifecycle({
}}
role="button"
>
- {getIconForLabel(
- event.label,
- "size-6 text-primary dark:text-white",
- )}
+
+ {getIconForLabel(
+ event.label,
+ "size-6 text-primary dark:text-white",
+ )}
+
{getTranslatedLabel(event.label)}
@@ -602,147 +651,79 @@ export default function ObjectLifecycle({
{t("detail.noObjectDetailData", { ns: "views/events" })}
) : (
-
- {eventSequence.map((item, idx) => {
- const isActive =
- Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5;
- const formattedEventTimestamp = config
- ? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
- timezone: config.ui.timezone,
- date_format:
- config.ui.time_format == "24hour"
- ? t(
- "time.formattedTimestampHourMinuteSecond.24hour",
- { ns: "common" },
- )
- : t(
- "time.formattedTimestampHourMinuteSecond.12hour",
- { ns: "common" },
- ),
- time_style: "medium",
- date_style: "medium",
- })
- : "";
+
+
+
+
+ {eventSequence.map((item, idx) => {
+ const isActive =
+ Math.abs((timeIndex ?? 0) - (item.timestamp ?? 0)) <= 0.5;
+ const formattedEventTimestamp = config
+ ? formatUnixTimestampToDateTime(item.timestamp ?? 0, {
+ timezone: config.ui.timezone,
+ date_format:
+ config.ui.time_format == "24hour"
+ ? t(
+ "time.formattedTimestampHourMinuteSecond.24hour",
+ { ns: "common" },
+ )
+ : t(
+ "time.formattedTimestampHourMinuteSecond.12hour",
+ { ns: "common" },
+ ),
+ time_style: "medium",
+ date_style: "medium",
+ })
+ : "";
- const ratio =
- Array.isArray(item.data.box) && item.data.box.length >= 4
- ? (
- aspectRatio *
- (item.data.box[2] / item.data.box[3])
- ).toFixed(2)
- : "N/A";
- const areaPx =
- Array.isArray(item.data.box) && item.data.box.length >= 4
- ? Math.round(
- (config.cameras[event.camera]?.detect?.width ?? 0) *
- (config.cameras[event.camera]?.detect?.height ??
- 0) *
- (item.data.box[2] * item.data.box[3]),
- )
- : undefined;
- const areaPct =
- Array.isArray(item.data.box) && item.data.box.length >= 4
- ? (item.data.box[2] * item.data.box[3]).toFixed(4)
- : undefined;
+ const ratio =
+ Array.isArray(item.data.box) && item.data.box.length >= 4
+ ? (
+ aspectRatio *
+ (item.data.box[2] / item.data.box[3])
+ ).toFixed(2)
+ : "N/A";
+ const areaPx =
+ Array.isArray(item.data.box) && item.data.box.length >= 4
+ ? Math.round(
+ (config.cameras[event.camera]?.detect?.width ?? 0) *
+ (config.cameras[event.camera]?.detect?.height ??
+ 0) *
+ (item.data.box[2] * item.data.box[3]),
+ )
+ : undefined;
+ const areaPct =
+ Array.isArray(item.data.box) && item.data.box.length >= 4
+ ? (item.data.box[2] * item.data.box[3]).toFixed(4)
+ : undefined;
- return (
-
{
- setTimeIndex(item.timestamp ?? 0);
- handleSetBox(
- item.data.box ?? [],
- item.data.attribute_box,
- );
- setLifecycleZones(item.data.zones);
- setSelectedZone("");
- }}
- className={cn(
- "flex cursor-pointer flex-col gap-1 rounded-md p-2 text-sm text-primary-variant",
- isActive
- ? "bg-secondary-highlight font-semibold text-primary outline-[1.5px] -outline-offset-[1.1px] outline-primary/40 dark:font-normal"
- : "duration-500",
- )}
- >
-
-
-
-
-
-
{getLifecycleItemDescription(item)}
-
- {formattedEventTimestamp}
-
-
-
-
-
-
-
-
- {t(
- "objectLifecycle.lifecycleItemDesc.header.ratio",
- )}
-
-
- {ratio}
-
-
-
-
-
- {t(
- "objectLifecycle.lifecycleItemDesc.header.area",
- )}
-
- {areaPx !== undefined && areaPct !== undefined ? (
-
- px: {areaPx} · %: {areaPct}
-
- ) : (
- N/A
- )}
-
- {item.class_type === "entered_zone" && (
-
-
- {t(
- "objectLifecycle.lifecycleItemDesc.header.zones",
- )}
-
-
- {item.data.zones.map((zone, zidx) => (
-
{
- e.stopPropagation();
- setSelectedZone(zone);
- }}
- >
-
-
- {zone.replaceAll("_", " ")}
-
-
- ))}
-
-
- )}
-
-
-
- );
- })}
+ return (
+
{
+ setTimeIndex(item.timestamp ?? 0);
+ handleSetBox(
+ item.data.box ?? [],
+ item.data.attribute_box,
+ );
+ setLifecycleZones(item.data.zones);
+ setSelectedZone("");
+ }}
+ setSelectedZone={setSelectedZone}
+ getZoneColor={getZoneColor}
+ />
+ );
+ })}
+
)}
@@ -789,3 +770,117 @@ export function LifecycleIcon({
return null;
}
}
+
+type LifecycleIconRowProps = {
+ item: ObjectLifecycleSequence;
+ isActive?: boolean;
+ formattedEventTimestamp: string;
+ ratio: string;
+ areaPx?: number;
+ areaPct?: string;
+ onClick: () => void;
+ setSelectedZone: (z: string) => void;
+ getZoneColor: (zoneName: string) => number[] | undefined;
+};
+
+function LifecycleIconRow({
+ item,
+ isActive,
+ formattedEventTimestamp,
+ ratio,
+ areaPx,
+ areaPct,
+ onClick,
+ setSelectedZone,
+ getZoneColor,
+}: LifecycleIconRowProps) {
+ const { t } = useTranslation(["views/explore"]);
+
+ return (
+
+
+
+
+
+
+
+
+
{getLifecycleItemDescription(item)}
+
+
+
+ {t("objectLifecycle.lifecycleItemDesc.header.ratio")}
+
+ {ratio}
+
+
+
+ {t("objectLifecycle.lifecycleItemDesc.header.area")}
+
+ {areaPx !== undefined && areaPct !== undefined ? (
+
+ {t("information.pixels", { ns: "common", area: areaPx })} ·{" "}
+ {areaPct}%
+
+ ) : (
+ N/A
+ )}
+
+
+ {item.data?.zones && item.data.zones.length > 0 && (
+
+ {item.data.zones.map((zone, zidx) => {
+ const color = getZoneColor(zone)?.join(",") ?? "0,0,0";
+ return (
+ {
+ e.stopPropagation();
+ setSelectedZone(zone);
+ }}
+ style={{
+ borderColor: `rgba(${color}, 0.6)`,
+ background: `rgba(${color}, 0.08)`,
+ }}
+ >
+
+
+ {zone.replaceAll("_", " ")}
+
+
+ );
+ })}
+
+ )}
+
+
+
+
{formattedEventTimestamp}
+
+
+
+ );
+}
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index f4bc75a5a..bdb78e0d3 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -66,6 +66,7 @@ export default function Events() {
camera: resp.data.camera,
startTime,
severity: resp.data.severity,
+ timelineType: "detail",
},
true,
);
diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx
index 677bb58e0..c01a0875a 100644
--- a/web/src/pages/FaceLibrary.tsx
+++ b/web/src/pages/FaceLibrary.tsx
@@ -858,14 +858,20 @@ function FaceAttemptGroup({
faceNames={faceNames}
onTrainAttempt={(name) => onTrainAttempt(data, name)}
>
-
+
- onReprocess(data)}
- />
+
+
+
onReprocess(data)}
+ />
+
{t("button.reprocessFace")}
diff --git a/web/src/types/record.ts b/web/src/types/record.ts
index a93029376..b662ce547 100644
--- a/web/src/types/record.ts
+++ b/web/src/types/record.ts
@@ -37,6 +37,7 @@ export type RecordingStartingPoint = {
camera: string;
startTime: number;
severity: ReviewSeverity;
+ timelineType?: "timeline" | "events" | "detail";
};
export type RecordingPlayerError = "stalled" | "startup";
diff --git a/web/src/views/classification/ModelTrainingView.tsx b/web/src/views/classification/ModelTrainingView.tsx
index f313d6829..ed96406e6 100644
--- a/web/src/views/classification/ModelTrainingView.tsx
+++ b/web/src/views/classification/ModelTrainingView.tsx
@@ -811,7 +811,10 @@ function StateTrainGrid({
image={data.filename}
onRefresh={onRefresh}
>
-
+
@@ -958,7 +961,10 @@ function ObjectTrainGrid({
image={data.filename}
onRefresh={onRefresh}
>
-
>
)}
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx
index 83c080555..f2c2606df 100644
--- a/web/src/views/events/EventView.tsx
+++ b/web/src/views/events/EventView.tsx
@@ -53,8 +53,6 @@ import { cn } from "@/lib/utils";
import { FilterList, LAST_24_HOURS_KEY } from "@/types/filter";
import { GiSoundWaves } from "react-icons/gi";
import useKeyboardListener from "@/hooks/use-keyboard-listener";
-import ReviewDetailDialog from "@/components/overlay/detail/ReviewDetailDialog";
-
import { useTimelineZoom } from "@/hooks/use-timeline-zoom";
import { useTranslation } from "react-i18next";
@@ -398,6 +396,7 @@ export default function EventView({
onSelectAllReviews={onSelectAllReviews}
setSelectedReviews={setSelectedReviews}
pullLatestData={pullLatestData}
+ onOpenRecording={onOpenRecording}
/>
)}
{severity == "significant_motion" && (
@@ -441,6 +440,7 @@ type DetectionReviewProps = {
onSelectAllReviews: () => void;
setSelectedReviews: (reviews: ReviewSegment[]) => void;
pullLatestData: () => void;
+ onOpenRecording: (recordingInfo: RecordingStartingPoint) => void;
};
function DetectionReview({
contentRef,
@@ -460,15 +460,12 @@ function DetectionReview({
onSelectAllReviews,
setSelectedReviews,
pullLatestData,
+ onOpenRecording,
}: DetectionReviewProps) {
const { t } = useTranslation(["views/events"]);
const reviewTimelineRef = useRef