frigate/web/src/components/overlay/chip/GenAISummaryChip.tsx
Nicolas Mowen 1b57fb15a7
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
Miscellaneous Fixes (#21063)
* Fix history management failing when updating URL

* Handle case where user doesn't have images that represent all states

If a user selects all imags and can't proceed we show a warning that they can still proceed but the model won't be trained until they get at least one image for every state.

* Still create all classes

We stil need to create all classes even if the user didn't assign images to them.

* fix camera group access for non admin users

changes from previous PR wrongly included users from the standard viewer role (but excluded custom viewer roles)

* Adjust threat level interaction to be less strict

* use base path when fetching go2rtc data

* show config error message when starting in safe mode

* fix genai migration

* fix genai

* Fix genai migration

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-11-27 07:58:35 -06:00

132 lines
3.7 KiB
TypeScript

import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";
import { cn } from "@/lib/utils";
import {
ReviewSegment,
ThreatLevel,
THREAT_LEVEL_LABELS,
} 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) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(review?.data?.metadata != undefined);
}, [review]);
return (
<div
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 p-2 text-sm transition-all duration-500",
isVisible ? "translate-y-0 opacity-100" : "-translate-y-4 opacity-0",
isDesktop ? "bg-card" : "bg-secondary-foreground",
)}
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 = "";
const threatLevel = aiAnalysis.potential_threat_level ?? 0;
if (threatLevel > 0) {
let label = "";
switch (threatLevel) {
case ThreatLevel.NEEDS_REVIEW:
label = t("needsReview", { ns: "views/events" });
break;
case ThreatLevel.SECURITY_CONCERN:
label = t("securityConcern", { ns: "views/events" });
break;
default:
label = THREAT_LEVEL_LABELS[threatLevel as ThreatLevel] || "Unknown";
}
concerns = `${label}\n`;
}
(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>
);
}