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 { 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 { getTranslatedLabel } from "@/utils/i18n"; type ClassificationCardProps = { className?: string; imgClassName?: string; data: ClassificationItemData; threshold?: ClassificationThreshold; selected: boolean; i18nLibrary: string; showArea?: boolean; onClick: (data: ClassificationItemData, meta: boolean) => void; children?: React.ReactNode; }; export function ClassificationCard({ className, imgClassName, data, threshold, selected, i18nLibrary, showArea = true, onClick, children, }: ClassificationCardProps) { 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 ( <>
setImageLoaded(true)} className={cn("size-44", imgClassName, isMobile && "w-full")} src={`${baseUrl}${data.filepath}`} onClick={(e) => { e.stopPropagation(); onClick(data, e.metaKey || e.ctrlKey); }} /> {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; onSelectEvent: (event: Event) => void; children?: (data: ClassificationItemData) => React.ReactNode; }; export function GroupedClassificationCard({ group, event, threshold, selectedItems, i18nLibrary, objectType, onClick, onSelectEvent, children, }: GroupedClassificationCardProps) { const navigate = useNavigate(); const { t } = useTranslation(["views/explore", i18nLibrary]); // data const allItemsSelected = useMemo( () => group.every((data) => selectedItems.includes(data.filename)), [group, selectedItems], ); const time = useMemo(() => { const item = group[0]; if (!item?.timestamp) { return undefined; } return item.timestamp * 1000; }, [group]); return (
{ if (selectedItems.length) { onClick(undefined); } }} onContextMenu={(e) => { e.stopPropagation(); e.preventDefault(); onClick(undefined); }} >
{getTranslatedLabel(objectType)} {event?.sub_label ? `: ${event.sub_label} (${Math.round((event.data.sub_label_score || 0) * 100)}%)` : ": " + t("details.unknown")}
{time && ( )}
{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); } else if (event) { onSelectEvent(event); } }} > {children?.(data)} ))}
); }