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:
Nicolas Mowen 2025-10-08 13:55:04 -06:00 committed by GitHub
parent 7a8f93e9f5
commit 28e3f83ae3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 37 additions and 22 deletions

View File

@ -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:

View File

@ -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={

View File

@ -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}
> >

View File

@ -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),
}} }}
> >
{isDesktop && (
<GenAISummaryDialog <GenAISummaryDialog
review={activeReviewItem} review={activeReviewItem}
onOpen={onAnalysisOpen} 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,