Review card refactor (#20813)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled

* Use the review card in event timeline popover

* Show review title in review card
This commit is contained in:
Nicolas Mowen 2025-11-05 08:48:47 -07:00 committed by GitHub
parent e1bc7360ad
commit a510ea9036
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 51 additions and 41 deletions

View File

@ -38,6 +38,7 @@ import { Button, buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { LuCircle } from "react-icons/lu"; import { LuCircle } from "react-icons/lu";
import { MdAutoAwesome } from "react-icons/md";
type ReviewCardProps = { type ReviewCardProps = {
event: ReviewSegment; event: ReviewSegment;
@ -164,29 +165,33 @@ export default function ReviewCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="flex items-center justify-evenly gap-1"> <div className="flex items-center gap-2">
<> <LuCircle
<LuCircle className={cn(
className={cn( "size-2",
"size-2", event.severity == "alert"
event.severity == "alert" ? "fill-severity_alert text-severity_alert"
? "fill-severity_alert text-severity_alert" : "fill-severity_detection text-severity_detection",
: "fill-severity_detection text-severity_detection", )}
)} />
/> <div className="flex items-center gap-1">
{event.data.objects.map((object) => { {event.data.objects.map((object, idx) => (
return getIconForLabel( <div
object, key={`${object}-${idx}`}
"size-3 text-primary dark:text-white", className="rounded-full bg-muted-foreground p-1"
); >
})} {getIconForLabel(object, "size-3 text-white")}
{event.data.audio.map((audio) => { </div>
return getIconForLabel( ))}
audio, {event.data.audio.map((audio, idx) => (
"size-3 text-primary dark:text-white", <div
); key={`${audio}-${idx}`}
})} className="rounded-full bg-muted-foreground p-1"
</> >
{getIconForLabel(audio, "size-3 text-white")}
</div>
))}
</div>
<div className="font-extra-light text-xs">{formattedDate}</div> <div className="font-extra-light text-xs">{formattedDate}</div>
</div> </div>
</TooltipTrigger> </TooltipTrigger>
@ -213,6 +218,14 @@ export default function ReviewCard({
dense dense
/> />
</div> </div>
{event.data.metadata?.title && (
<div className="flex items-center gap-1.5 rounded bg-secondary/50">
<MdAutoAwesome className="size-3 shrink-0 text-primary" />
<span className="truncate text-xs text-primary">
{event.data.metadata.title}
</span>
</div>
)}
</div> </div>
); );

View File

@ -22,6 +22,7 @@ import {
LuChevronRight, LuChevronRight,
LuSettings, LuSettings,
} from "react-icons/lu"; } from "react-icons/lu";
import { MdAutoAwesome } from "react-icons/md";
import { getTranslatedLabel } from "@/utils/i18n"; import { getTranslatedLabel } from "@/utils/i18n";
import EventMenu from "@/components/timeline/EventMenu"; import EventMenu from "@/components/timeline/EventMenu";
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog"; import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
@ -410,8 +411,9 @@ function ReviewGroup({
</div> </div>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
{review.data.metadata?.title && ( {review.data.metadata?.title && (
<div className="mb-1 text-sm text-primary-variant"> <div className="mb-1 flex items-center gap-1 text-sm text-primary-variant">
{review.data.metadata.title} <MdAutoAwesome className="size-3 shrink-0" />
<span className="truncate">{review.data.metadata.title}</span>
</div> </div>
)} )}
<div className="flex flex-row items-center gap-1.5"> <div className="flex flex-row items-center gap-1.5">

View File

@ -1,4 +1,3 @@
import { useApiHost } from "@/api";
import { useTimelineUtils } from "@/hooks/use-timeline-utils"; import { useTimelineUtils } from "@/hooks/use-timeline-utils";
import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils"; import { useEventSegmentUtils } from "@/hooks/use-event-segment-utils";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { ReviewSegment, ReviewSeverity } from "@/types/review";
@ -18,6 +17,7 @@ import { HoverCardPortal } from "@radix-ui/react-hover-card";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import { MinimapBounds, Tick, Timestamp } from "./segment-metadata"; import { MinimapBounds, Tick, Timestamp } from "./segment-metadata";
import useTapUtils from "@/hooks/use-tap-utils"; import useTapUtils from "@/hooks/use-tap-utils";
import ReviewCard from "../card/ReviewCard";
type EventSegmentProps = { type EventSegmentProps = {
events: ReviewSegment[]; events: ReviewSegment[];
@ -54,7 +54,7 @@ export function EventSegment({
displaySeverityType, displaySeverityType,
shouldShowRoundedCorners, shouldShowRoundedCorners,
getEventStart, getEventStart,
getEventThumbnail, getEvent,
} = useEventSegmentUtils(segmentDuration, events, severityType); } = useEventSegmentUtils(segmentDuration, events, severityType);
const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils( const { alignStartDateToTimeline, alignEndDateToTimeline } = useTimelineUtils(
@ -87,13 +87,11 @@ export function EventSegment({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [getEventStart, segmentTime]); }, [getEventStart, segmentTime]);
const apiHost = useApiHost();
const { handleTouchStart } = useTapUtils(); const { handleTouchStart } = useTapUtils();
const eventThumbnail = useMemo(() => { const segmentEvent = useMemo(() => {
return getEventThumbnail(segmentTime); return getEvent(segmentTime);
}, [getEventThumbnail, segmentTime]); }, [getEvent, segmentTime]);
const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]);
const segmentKey = useMemo( const segmentKey = useMemo(
@ -252,10 +250,7 @@ export function EventSegment({
className="w-[250px] rounded-lg p-2 md:rounded-2xl" className="w-[250px] rounded-lg p-2 md:rounded-2xl"
side="left" side="left"
> >
<img {segmentEvent && <ReviewCard event={segmentEvent} />}
className="rounded-lg"
src={`${apiHost}${eventThumbnail.replace("/media/frigate/", "")}`}
/>
</HoverCardContent> </HoverCardContent>
</HoverCardPortal> </HoverCardPortal>
</HoverCard> </HoverCard>

View File

@ -191,8 +191,8 @@ export const useEventSegmentUtils = (
[events, getSegmentStart, getSegmentEnd, severityType], [events, getSegmentStart, getSegmentEnd, severityType],
); );
const getEventThumbnail = useCallback( const getEvent = useCallback(
(time: number): string => { (time: number): ReviewSegment | undefined => {
const matchingEvent = events.find((event) => { const matchingEvent = events.find((event) => {
return ( return (
time >= getSegmentStart(event.start_time) && time >= getSegmentStart(event.start_time) &&
@ -201,7 +201,7 @@ export const useEventSegmentUtils = (
); );
}); });
return matchingEvent?.thumb_path ?? ""; return matchingEvent;
}, },
[events, getSegmentStart, getSegmentEnd, severityType], [events, getSegmentStart, getSegmentEnd, severityType],
); );
@ -214,6 +214,6 @@ export const useEventSegmentUtils = (
getReviewed, getReviewed,
shouldShowRoundedCorners, shouldShowRoundedCorners,
getEventStart, getEventStart,
getEventThumbnail, getEvent,
}; };
}; };

View File

@ -974,7 +974,7 @@ function Timeline({
? "w-[100px] flex-shrink-0" ? "w-[100px] flex-shrink-0"
: timelineType == "detail" : timelineType == "detail"
? "min-w-[20rem] max-w-[30%] flex-shrink-0 flex-grow-0 basis-[30rem] md:min-w-[20rem] md:max-w-[25%] lg:min-w-[30rem] lg:max-w-[33%]" ? "min-w-[20rem] max-w-[30%] flex-shrink-0 flex-grow-0 basis-[30rem] md:min-w-[20rem] md:max-w-[25%] lg:min-w-[30rem] lg:max-w-[33%]"
: "w-60 flex-shrink-0", : "w-80 flex-shrink-0",
) )
: cn( : cn(
timelineType == "timeline" timelineType == "timeline"