import { baseUrl } from "@/api/baseUrl"; import useContextMenu from "@/hooks/use-contextmenu"; import { cn } from "@/lib/utils"; import { ClassificationItemData, ClassificationThreshold, } from "@/types/classification"; import { Event } from "@/types/event"; import { forwardRef, useMemo, useRef, useState } from "react"; import { isDesktop, isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import TimeAgo from "../dynamic/TimeAgo"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; import { LuSearch } from "react-icons/lu"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { useNavigate } from "react-router-dom"; import { HiSquare2Stack } from "react-icons/hi2"; import { ImageShadowOverlay } from "../overlay/ImageShadowOverlay"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "../ui/dialog"; import { MobilePage, MobilePageContent, MobilePageDescription, MobilePageHeader, MobilePageTitle, MobilePageTrigger, } from "../mobile/MobilePage"; type ClassificationCardProps = { className?: string; imgClassName?: string; data: ClassificationItemData; threshold?: ClassificationThreshold; selected: boolean; i18nLibrary: string; showArea?: boolean; count?: number; onClick: (data: ClassificationItemData, meta: boolean) => void; children?: React.ReactNode; }; export const ClassificationCard = forwardRef< HTMLDivElement, ClassificationCardProps >(function ClassificationCard( { className, imgClassName, data, threshold, selected, i18nLibrary, showArea = true, count, onClick, children, }, ref, ) { const { t } = useTranslation([i18nLibrary]); const [imageLoaded, setImageLoaded] = useState(false); const scoreStatus = useMemo(() => { if (!data.score || !threshold) { return "unknown"; } if (data.score >= threshold.recognition) { return "match"; } else if (data.score >= threshold.unknown) { return "potential"; } else { return "unknown"; } }, [data, threshold]); // interaction const imgRef = useRef(null); useContextMenu(imgRef, () => { onClick(data, true); }); const imageArea = useMemo(() => { if (!showArea || imgRef.current == null || !imageLoaded) { return undefined; } return imgRef.current.naturalWidth * imgRef.current.naturalHeight; }, [showArea, imageLoaded]); return (
{ const isMeta = e.metaKey || e.ctrlKey; if (isMeta) { e.stopPropagation(); } onClick(data, isMeta); }} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onClick(data, true); }} > setImageLoaded(true)} src={`${baseUrl}${data.filepath}`} /> {count && (
{count}
{" "}
)} {!count && imageArea != undefined && (
{t("information.pixels", { ns: "common", area: imageArea })}
)}
{data.name == "unknown" ? t("details.unknown") : data.name}
{data.score && (
{Math.round(data.score * 100)}%
)}
{children}
); }); type GroupedClassificationCardProps = { group: ClassificationItemData[]; event?: Event; threshold?: ClassificationThreshold; selectedItems: string[]; i18nLibrary: string; objectType: string; onClick: (data: ClassificationItemData | undefined) => void; children?: (data: ClassificationItemData) => React.ReactNode; }; export function GroupedClassificationCard({ group, event, threshold, selectedItems, i18nLibrary, onClick, children, }: GroupedClassificationCardProps) { const navigate = useNavigate(); const { t } = useTranslation(["views/explore", i18nLibrary]); const [detailOpen, setDetailOpen] = useState(false); // data const bestItem = useMemo(() => { let best: undefined | ClassificationItemData = undefined; group.forEach((item) => { if (item?.name != undefined && item.name != "none") { if ( best?.score == undefined || (item.score && best.score < item.score) ) { best = item; } } }); if (!best) { return group.at(-1); } const bestTyped: ClassificationItemData = best; return { ...bestTyped, name: event ? (event.sub_label ?? t("details.unknown")) : bestTyped.name, score: event?.data?.sub_label_score || bestTyped.score, }; }, [group, event, t]); const bestScoreStatus = useMemo(() => { if (!bestItem?.score || !threshold) { return "unknown"; } if (bestItem.score >= threshold.recognition) { return "match"; } else if (bestItem.score >= threshold.unknown) { return "potential"; } else { return "unknown"; } }, [bestItem, threshold]); const time = useMemo(() => { const item = group[0]; if (!item?.timestamp) { return undefined; } return item.timestamp * 1000; }, [group]); if (!bestItem) { return null; } const Overlay = isDesktop ? Dialog : MobilePage; const Trigger = isDesktop ? DialogTrigger : MobilePageTrigger; const Header = isDesktop ? DialogHeader : MobilePageHeader; const Content = isDesktop ? DialogContent : MobilePageContent; const ContentTitle = isDesktop ? DialogTitle : MobilePageTitle; const ContentDescription = isDesktop ? DialogDescription : MobilePageDescription; return ( <> { if (meta || selectedItems.length > 0) { onClick(undefined); } else { setDetailOpen(true); } }} /> { if (!open) { setDetailOpen(false); } }} > e.preventDefault()} > <>
{event?.sub_label ? event.sub_label : t("details.unknown")} {event?.sub_label && (
{`${Math.round((event.data.sub_label_score || 0) * 100)}%`}
)}
{time && ( )}
{isDesktop && (
{event && (
{ navigate(`/explore?event_id=${event.id}`); }} >
{t("details.item.button.viewInExplore", { ns: "views/explore", })}
)}
)}
{group.map((data: ClassificationItemData) => (
{ if (meta || selectedItems.length > 0) { onClick(data); } }} > {children?.(data)}
))}
); }