mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
Review genai improvements (#20387)
* Add padding to when genai popup shows * Move popup to timeline for mobile * Improve UI * Use genai title for notification
This commit is contained in:
parent
7a8f93e9f5
commit
28e3f83ae3
@ -371,13 +371,14 @@ class WebPushClient(Communicator):
|
|||||||
|
|
||||||
sorted_objects.update(payload["after"]["data"]["sub_labels"])
|
sorted_objects.update(payload["after"]["data"]["sub_labels"])
|
||||||
|
|
||||||
title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}"
|
|
||||||
image = f"{payload['after']['thumb_path'].replace('/media/frigate', '')}"
|
image = f"{payload['after']['thumb_path'].replace('/media/frigate', '')}"
|
||||||
ended = state == "end" or state == "genai"
|
ended = state == "end" or state == "genai"
|
||||||
|
|
||||||
if state == "genai" and payload["after"]["data"]["metadata"]:
|
if state == "genai" and payload["after"]["data"]["metadata"]:
|
||||||
|
title = payload["after"]["data"]["metadata"]["title"]
|
||||||
message = payload["after"]["data"]["metadata"]["scene"]
|
message = payload["after"]["data"]["metadata"]["scene"]
|
||||||
else:
|
else:
|
||||||
|
title = f"{titlecase(', '.join(sorted_objects).replace('_', ' '))}{' was' if state == 'end' else ''} detected in {titlecase(', '.join(payload['after']['data']['zones']).replace('_', ' '))}"
|
||||||
message = f"Detected on {camera_name}"
|
message = f"Detected on {camera_name}"
|
||||||
|
|
||||||
if ended:
|
if ended:
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { getIconForLabel } from "@/utils/iconUtil";
|
|||||||
import { isDesktop, isIOS, isSafari } from "react-device-detect";
|
import { isDesktop, isIOS, isSafari } from "react-device-detect";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import TimeAgo from "../dynamic/TimeAgo";
|
import TimeAgo from "../dynamic/TimeAgo";
|
||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import useImageLoaded from "@/hooks/use-image-loaded";
|
import useImageLoaded from "@/hooks/use-image-loaded";
|
||||||
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
||||||
import { FaCompactDisc } from "react-icons/fa";
|
import { FaCompactDisc } from "react-icons/fa";
|
||||||
@ -36,15 +36,16 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|||||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||||
import { buttonVariants } from "../ui/button";
|
import { buttonVariants } from "../ui/button";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ReviewCardProps = {
|
type ReviewCardProps = {
|
||||||
event: ReviewSegment;
|
event: ReviewSegment;
|
||||||
currentTime: number;
|
activeReviewItem?: ReviewSegment;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
export default function ReviewCard({
|
export default function ReviewCard({
|
||||||
event,
|
event,
|
||||||
currentTime,
|
activeReviewItem,
|
||||||
onClick,
|
onClick,
|
||||||
}: ReviewCardProps) {
|
}: ReviewCardProps) {
|
||||||
const { t } = useTranslation(["components/dialog"]);
|
const { t } = useTranslation(["components/dialog"]);
|
||||||
@ -57,12 +58,6 @@ export default function ReviewCard({
|
|||||||
: t("time.formattedTimestampHourMinute.12hour", { ns: "common" }),
|
: t("time.formattedTimestampHourMinute.12hour", { ns: "common" }),
|
||||||
config?.ui.timezone,
|
config?.ui.timezone,
|
||||||
);
|
);
|
||||||
const isSelected = useMemo(
|
|
||||||
() =>
|
|
||||||
event.start_time <= currentTime &&
|
|
||||||
(event.end_time ?? Date.now() / 1000) >= currentTime,
|
|
||||||
[event, currentTime],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@ -139,7 +134,12 @@ export default function ReviewCard({
|
|||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
className={`size-full rounded-lg ${isSelected ? "outline outline-[3px] outline-offset-1 outline-selected" : ""} ${imgLoaded ? "visible" : "invisible"}`}
|
className={cn(
|
||||||
|
"size-full rounded-lg",
|
||||||
|
activeReviewItem?.id == event.id &&
|
||||||
|
"outline outline-[3px] outline-offset-1 outline-selected",
|
||||||
|
imgLoaded ? "visible" : "invisible",
|
||||||
|
)}
|
||||||
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
||||||
loading={isSafari ? "eager" : "lazy"}
|
loading={isSafari ? "eager" : "lazy"}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@ -21,8 +21,9 @@ export function GenAISummaryChip({ review, onClick }: GenAISummaryChipProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-1/2 top-8 z-30 flex max-w-[90vw] -translate-x-[50%] cursor-pointer select-none items-center gap-2 rounded-full bg-card p-2 text-sm transition-all duration-500",
|
"absolute left-1/2 top-8 z-30 flex max-w-[90vw] -translate-x-[50%] cursor-pointer select-none items-center gap-2 rounded-full p-2 text-sm transition-all duration-500",
|
||||||
isVisible ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0",
|
isVisible ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0",
|
||||||
|
isDesktop ? "bg-card" : "bg-secondary-foreground",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -466,9 +466,9 @@ export function RecordingView({
|
|||||||
|
|
||||||
return mainCameraReviewItems.find(
|
return mainCameraReviewItems.find(
|
||||||
(rev) =>
|
(rev) =>
|
||||||
rev.start_time < currentTime &&
|
rev.start_time - REVIEW_PADDING < currentTime &&
|
||||||
rev.end_time &&
|
rev.end_time &&
|
||||||
currentTime < rev.end_time,
|
currentTime < rev.end_time + REVIEW_PADDING,
|
||||||
);
|
);
|
||||||
}, [config, currentTime, mainCameraReviewItems, mainCamera]);
|
}, [config, currentTime, mainCameraReviewItems, mainCamera]);
|
||||||
const onAnalysisOpen = useCallback(
|
const onAnalysisOpen = useCallback(
|
||||||
@ -678,10 +678,12 @@ export function RecordingView({
|
|||||||
: Math.max(1, getCameraAspect(mainCamera) ?? 0),
|
: Math.max(1, getCameraAspect(mainCamera) ?? 0),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GenAISummaryDialog
|
{isDesktop && (
|
||||||
review={activeReviewItem}
|
<GenAISummaryDialog
|
||||||
onOpen={onAnalysisOpen}
|
review={activeReviewItem}
|
||||||
/>
|
onOpen={onAnalysisOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<DynamicVideoPlayer
|
<DynamicVideoPlayer
|
||||||
className={grow}
|
className={grow}
|
||||||
@ -773,12 +775,14 @@ export function RecordingView({
|
|||||||
}
|
}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
mainCameraReviewItems={mainCameraReviewItems}
|
mainCameraReviewItems={mainCameraReviewItems}
|
||||||
|
activeReviewItem={activeReviewItem}
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
exportRange={exportMode == "timeline" ? exportRange : undefined}
|
exportRange={exportMode == "timeline" ? exportRange : undefined}
|
||||||
setCurrentTime={setCurrentTime}
|
setCurrentTime={setCurrentTime}
|
||||||
manuallySetCurrentTime={manuallySetCurrentTime}
|
manuallySetCurrentTime={manuallySetCurrentTime}
|
||||||
setScrubbing={setScrubbing}
|
setScrubbing={setScrubbing}
|
||||||
setExportRange={setExportRange}
|
setExportRange={setExportRange}
|
||||||
|
onAnalysisOpen={onAnalysisOpen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -792,12 +796,14 @@ type TimelineProps = {
|
|||||||
timelineType: TimelineType;
|
timelineType: TimelineType;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
mainCameraReviewItems: ReviewSegment[];
|
mainCameraReviewItems: ReviewSegment[];
|
||||||
|
activeReviewItem?: ReviewSegment;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
exportRange?: TimeRange;
|
exportRange?: TimeRange;
|
||||||
setCurrentTime: React.Dispatch<React.SetStateAction<number>>;
|
setCurrentTime: React.Dispatch<React.SetStateAction<number>>;
|
||||||
manuallySetCurrentTime: (time: number, force: boolean) => void;
|
manuallySetCurrentTime: (time: number, force: boolean) => void;
|
||||||
setScrubbing: React.Dispatch<React.SetStateAction<boolean>>;
|
setScrubbing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setExportRange: (range: TimeRange) => void;
|
setExportRange: (range: TimeRange) => void;
|
||||||
|
onAnalysisOpen: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
function Timeline({
|
function Timeline({
|
||||||
contentRef,
|
contentRef,
|
||||||
@ -806,12 +812,14 @@ function Timeline({
|
|||||||
timelineType,
|
timelineType,
|
||||||
timeRange,
|
timeRange,
|
||||||
mainCameraReviewItems,
|
mainCameraReviewItems,
|
||||||
|
activeReviewItem,
|
||||||
currentTime,
|
currentTime,
|
||||||
exportRange,
|
exportRange,
|
||||||
setCurrentTime,
|
setCurrentTime,
|
||||||
manuallySetCurrentTime,
|
manuallySetCurrentTime,
|
||||||
setScrubbing,
|
setScrubbing,
|
||||||
setExportRange,
|
setExportRange,
|
||||||
|
onAnalysisOpen,
|
||||||
}: TimelineProps) {
|
}: TimelineProps) {
|
||||||
const { t } = useTranslation(["views/events"]);
|
const { t } = useTranslation(["views/events"]);
|
||||||
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
const internalTimelineRef = useRef<HTMLDivElement>(null);
|
||||||
@ -889,12 +897,17 @@ function Timeline({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={cn(
|
||||||
|
"relative",
|
||||||
isDesktop
|
isDesktop
|
||||||
? `${timelineType == "timeline" ? "w-[100px]" : "w-60"} no-scrollbar overflow-y-auto`
|
? `${timelineType == "timeline" ? "w-[100px]" : "w-60"} no-scrollbar overflow-y-auto`
|
||||||
: `overflow-hidden portrait:flex-grow ${timelineType == "timeline" ? "landscape:w-[100px]" : "landscape:w-[175px]"} `
|
: `overflow-hidden portrait:flex-grow ${timelineType == "timeline" ? "landscape:w-[100px]" : "landscape:w-[175px]"}`,
|
||||||
} relative`}
|
)}
|
||||||
>
|
>
|
||||||
|
{isMobile && (
|
||||||
|
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent"></div>
|
<div className="pointer-events-none absolute inset-x-0 top-0 z-20 h-[30px] w-full bg-gradient-to-b from-secondary to-transparent"></div>
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent"></div>
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-[30px] w-full bg-gradient-to-t from-secondary to-transparent"></div>
|
||||||
{timelineType == "timeline" ? (
|
{timelineType == "timeline" ? (
|
||||||
@ -946,7 +959,7 @@ function Timeline({
|
|||||||
<ReviewCard
|
<ReviewCard
|
||||||
key={review.id}
|
key={review.id}
|
||||||
event={review}
|
event={review}
|
||||||
currentTime={currentTime}
|
activeReviewItem={activeReviewItem}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
manuallySetCurrentTime(
|
manuallySetCurrentTime(
|
||||||
review.start_time - REVIEW_PADDING,
|
review.start_time - REVIEW_PADDING,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user