Add popup for genai review summary

This commit is contained in:
Nicolas Mowen 2025-10-07 15:48:16 -06:00
parent 5c914e25eb
commit 15e0f5b537
3 changed files with 142 additions and 0 deletions

View File

@ -0,0 +1,112 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { cn } from "@/lib/utils";
import { ReviewSegment, ThreatLevel } from "@/types/review";
import { useEffect, useMemo, useState } from "react";
import { isDesktop } from "react-device-detect";
import { useTranslation } from "react-i18next";
import { MdAutoAwesome } from "react-icons/md";
type GenAISummaryChipProps = {
review?: ReviewSegment;
onClick: () => void;
};
export function GenAISummaryChip({ review, onClick }: GenAISummaryChipProps) {
if (!review?.data?.metadata) {
return null;
}
return (
<div
className="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"
onClick={onClick}
>
<MdAutoAwesome className="shrink-0" />
<span className="truncate">{review.data.metadata.title}</span>
</div>
);
}
type GenAISummaryDialogProps = {
review?: ReviewSegment;
onOpen?: (open: boolean) => void;
};
export function GenAISummaryDialog({
review,
onOpen,
}: GenAISummaryDialogProps) {
const { t } = useTranslation(["views/explore"]);
// data
const aiAnalysis = useMemo(() => review?.data?.metadata, [review]);
const aiThreatLevel = useMemo(() => {
if (
!aiAnalysis ||
(!aiAnalysis.potential_threat_level && !aiAnalysis.other_concerns)
) {
return "None";
}
let concerns = "";
switch (aiAnalysis.potential_threat_level) {
case ThreatLevel.SUSPICIOUS:
concerns = `${t("suspiciousActivity", { ns: "views/events" })}\n`;
break;
case ThreatLevel.DANGER:
concerns = `${t("threateningActivity", { ns: "views/events" })}\n`;
break;
}
(aiAnalysis.other_concerns ?? []).forEach((c) => {
concerns += `${c}\n`;
});
return concerns || "None";
}, [aiAnalysis, t]);
// layout
const [open, setOpen] = useState(false);
const Overlay = isDesktop ? Dialog : Drawer;
const Trigger = isDesktop ? DialogTrigger : DrawerTrigger;
const Content = isDesktop ? DialogContent : DrawerContent;
useEffect(() => {
if (onOpen) {
onOpen(open);
}
}, [open, onOpen]);
if (!aiAnalysis) {
return null;
}
return (
<Overlay open={open} onOpenChange={setOpen}>
<Trigger asChild>
<GenAISummaryChip review={review} onClick={() => setOpen(true)} />
</Trigger>
<Content
className={cn(
"gap-2",
isDesktop
? "sm:rounded-lg md:rounded-2xl"
: "mx-4 rounded-lg px-4 pb-4 md:rounded-2xl",
)}
>
{t("aiAnalysis.title")}
<div className="text-sm text-primary/40">
{t("details.description.label")}
</div>
<div className="text-sm">{aiAnalysis.scene}</div>
<div className="text-sm text-primary/40">
{t("details.score.label")}
</div>
<div className="text-sm">{aiAnalysis.confidence * 100}%</div>
<div className="text-sm text-primary/40">{t("concerns.label")}</div>
<div className="text-sm">{aiThreatLevel}</div>
</Content>
</Overlay>
);
}

View File

@ -19,6 +19,7 @@ export type ReviewData = {
significant_motion_areas: number[];
zones: string[];
metadata?: {
title: string;
scene: string;
confidence: number;
potential_threat_level?: number;

View File

@ -66,6 +66,7 @@ import {
} from "@/components/ui/tooltip";
import { CameraNameLabel } from "@/components/camera/CameraNameLabel";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { GenAISummaryDialog } from "@/components/overlay/chip/GenAISummaryChip";
type RecordingViewProps = {
startCamera: string;
@ -458,6 +459,29 @@ export function RecordingView({
[visiblePreviewObserver.current],
);
const activeReviewItem = useMemo(() => {
if (!config?.cameras?.[mainCamera].review.genai?.enabled_in_config) {
return undefined;
}
return mainCameraReviewItems.find(
(rev) =>
rev.start_time < currentTime &&
rev.end_time &&
currentTime < rev.end_time,
);
}, [config, currentTime, mainCameraReviewItems, mainCamera]);
const onAnalysisOpen = useCallback(
(open: boolean) => {
if (open) {
mainControllerRef.current?.pause();
} else {
mainControllerRef.current?.play();
}
},
[mainControllerRef],
);
return (
<div ref={contentRef} className="flex size-full flex-col pt-2">
<Toaster closeButton={true} />
@ -654,6 +678,11 @@ export function RecordingView({
: Math.max(1, getCameraAspect(mainCamera) ?? 0),
}}
>
<GenAISummaryDialog
review={activeReviewItem}
onOpen={onAnalysisOpen}
/>
<DynamicVideoPlayer
className={grow}
camera={mainCamera}