From 1fec95f88e0f97d4244e9f0b0fd36cc37f2c6a4e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 8 Feb 2026 07:02:40 -0600 Subject: [PATCH] improve chip tooltip display - use formatList to use i18n separators instead of commas - ensure the correct event type is used so sublabels are not run through normalization - remove smart-capitalization classes as translated labels use i18n (which includes capitalization) - give icons an optional key so that the console doesn't complain about duplication when rendering --- web/src/components/card/AnimatedEventCard.tsx | 39 ++++++--- web/src/components/card/ReviewCard.tsx | 37 ++++---- web/src/components/player/LivePlayer.tsx | 29 ++++--- .../player/PreviewThumbnailPlayer.tsx | 53 ++++++------ web/src/utils/iconUtil.tsx | 85 ++++++++++--------- 5 files changed, 137 insertions(+), 106 deletions(-) diff --git a/web/src/components/card/AnimatedEventCard.tsx b/web/src/components/card/AnimatedEventCard.tsx index c5da99aa2..b6e9ddf7c 100644 --- a/web/src/components/card/AnimatedEventCard.tsx +++ b/web/src/components/card/AnimatedEventCard.tsx @@ -19,6 +19,8 @@ import { Button } from "../ui/button"; import { FaCircleCheck } from "react-icons/fa6"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { formatList } from "@/utils/stringUtil"; type AnimatedEventCardProps = { event: ReviewSegment; @@ -50,26 +52,37 @@ export function AnimatedEventCard({ fetchPreviews: !currentHour, }); + const getEventType = useCallback( + (text: string) => { + if (event.data.sub_labels?.includes(text)) return "manual"; + if (event.data.audio.includes(text)) return "audio"; + return "object"; + }, + [event], + ); + const tooltipText = useMemo(() => { if (event?.data?.metadata?.title) { return event.data.metadata.title; } return ( - `${[ - ...new Set([ - ...(event.data.objects || []), - ...(event.data.sub_labels || []), - ...(event.data.audio || []), - ]), - ] - .filter((item) => item !== undefined && !item.includes("-verified")) - .map((text) => text.charAt(0).toUpperCase() + text.substring(1)) - .sort() - .join(", ") - .replaceAll("-verified", "")} ` + t("detected") + `${formatList( + [ + ...new Set([ + ...(event.data.objects || []).map((text) => + text.replace("-verified", ""), + ), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter((item) => item !== undefined) + .map((text) => getTranslatedLabel(text, getEventType(text))) + .sort(), + )} ` + t("detected") ); - }, [event, t]); + }, [event, getEventType, t]); // visibility diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index b5ba5cfea..89d79dee7 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -33,13 +33,14 @@ import axios from "axios"; import { toast } from "sonner"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; -import { capitalizeFirstLetter } from "@/utils/stringUtil"; import { Button, buttonVariants } from "../ui/button"; import { Trans, useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; import { LuCircle } from "react-icons/lu"; import { MdAutoAwesome } from "react-icons/md"; import { GenAISummaryDialog } from "../overlay/chip/GenAISummaryChip"; +import { getTranslatedLabel } from "@/utils/i18n"; +import { formatList } from "@/utils/stringUtil"; type ReviewCardProps = { event: ReviewSegment; @@ -123,6 +124,12 @@ export default function ReviewCard({ } }, [bypassDialogRef, onDelete]); + const getEventType = (text: string) => { + if (event.data.sub_labels?.includes(text)) return "manual"; + if (event.data.audio.includes(text)) return "audio"; + return "object"; + }; + const content = (
- {[ - ...new Set([ - ...(event.data.objects || []), - ...(event.data.sub_labels || []), - ...(event.data.audio || []), - ]), - ] - .filter( - (item) => item !== undefined && !item.includes("-verified"), - ) - .map((text) => capitalizeFirstLetter(text)) - .sort() - .join(", ") - .replaceAll("-verified", "")} + {formatList( + [ + ...new Set([ + ...(event.data.objects || []).map((text) => + text.replace("-verified", ""), + ), + ...(event.data.sub_labels || []), + ...(event.data.audio || []), + ]), + ] + .filter((item) => item !== undefined) + .map((text) => getTranslatedLabel(text, getEventType(text))) + .sort(), + )}
- + {formatList( [ - ...new Set([ - ...(objects || []).map(({ label, sub_label }) => - label.endsWith("verified") - ? sub_label - : label.replaceAll("_", " "), - ), - ]), - ] - .filter((label) => label?.includes("-verified") == false) - .map((label) => - getTranslatedLabel(label.replace("-verified", "")), - ) - .sort(), + ...new Set( + (objects || []) + .map(({ label, sub_label }) => { + const isManual = label.endsWith("verified"); + const text = isManual ? sub_label : label; + const type = isManual ? "manual" : "object"; + return getTranslatedLabel(text, type); + }) + .filter( + (translated) => + translated && !translated.includes("-verified"), + ), + ), + ].sort(), )} diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index 42a00b86c..d612a1566 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -28,6 +28,7 @@ import { useTranslation } from "react-i18next"; import { FaExclamationTriangle } from "react-icons/fa"; import { MdOutlinePersonSearch } from "react-icons/md"; import { getTranslatedLabel } from "@/utils/i18n"; +import { formatList } from "@/utils/stringUtil"; type PreviewPlayerProps = { review: ReviewSegment; @@ -182,9 +183,8 @@ export default function PreviewThumbnailPlayer({ ); const getEventType = (text: string) => { - if (review.data.objects.includes(text)) return "object"; - if (review.data.audio.includes(text)) return "audio"; if (review.data.sub_labels?.includes(text)) return "manual"; + if (review.data.audio.includes(text)) return "audio"; return "object"; }; @@ -268,13 +268,16 @@ export default function PreviewThumbnailPlayer({ className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} bg-gradient-to-br ${review.has_been_reviewed ? "bg-green-600 from-green-600 to-green-700" : "bg-gray-500 from-gray-400 to-gray-500"} z-0`} onClick={() => onClick(review, false, true)} > - {review.data.objects.sort().map((object) => { - return getIconForLabel( - object, - "object", - "size-3 text-white", - ); - })} + {review.data.objects + .sort() + .map((object, idx) => + getIconForLabel( + object, + "object", + "size-3 text-white", + `${object}-${idx}`, + ), + )} {review.data.audio.map((audio) => { return getIconForLabel( audio, @@ -288,23 +291,25 @@ export default function PreviewThumbnailPlayer({ - + {review.data.metadata ? review.data.metadata.title - : [ - ...new Set([ - ...(review.data.objects || []), - ...(review.data.sub_labels || []), - ...(review.data.audio || []), - ]), - ] - .filter( - (item) => - item !== undefined && !item.includes("-verified"), - ) - .map((text) => getTranslatedLabel(text, getEventType(text))) - .sort() - .join(", ")} + : formatList( + [ + ...new Set([ + ...(review.data.objects || []).map((text) => + text.replace("-verified", ""), + ), + ...(review.data.sub_labels || []), + ...(review.data.audio || []), + ]), + ] + .filter((item) => item !== undefined) + .map((text) => + getTranslatedLabel(text, getEventType(text)), + ) + .sort(), + )} {!!( diff --git a/web/src/utils/iconUtil.tsx b/web/src/utils/iconUtil.tsx index 8ddf3ea08..04324aabe 100644 --- a/web/src/utils/iconUtil.tsx +++ b/web/src/utils/iconUtil.tsx @@ -62,83 +62,86 @@ export function getIconForLabel( label: string, type: EventType = "object", className?: string, + key?: string, ) { + const iconKey = key || label; + if (label.endsWith("-verified")) { - return getVerifiedIcon(label, className, type); + return getVerifiedIcon(label, className, type, iconKey); } else if (label.endsWith("-plate")) { - return getRecognizedPlateIcon(label, className, type); + return getRecognizedPlateIcon(label, className, type, iconKey); } switch (label) { // objects case "bear": - return ; + return ; case "bicycle": - return ; + return ; case "bird": - return ; + return ; case "boat": - return ; + return ; case "bus": case "school_bus": - return ; + return ; case "car": case "vehicle": - return ; + return ; case "cat": - return ; + return ; case "deer": - return ; + return ; case "animal": case "bark": case "dog": - return ; + return ; case "fox": - return ; + return ; case "goat": - return ; + return ; case "horse": - return ; + return ; case "kangaroo": - return ; + return ; case "license_plate": - return ; + return ; case "motorcycle": - return ; + return ; case "mouse": - return ; + return ; case "package": - return ; + return ; case "person": - return ; + return ; case "rabbit": - return ; + return ; case "raccoon": - return ; + return ; case "robot_lawnmower": - return ; + return ; case "sports_ball": - return ; + return ; case "skunk": - return ; + return ; case "squirrel": - return ; + return ; case "umbrella": - return ; + return ; case "waste_bin": - return ; + return ; // audio case "crying": case "laughter": case "scream": case "speech": case "yell": - return ; + return ; case "fire_alarm": - return ; + return ; // sub labels case "amazon": - return ; + return ; case "an_post": case "canada_post": case "dpd": @@ -148,20 +151,20 @@ export function getIconForLabel( case "postnord": case "purolator": case "royal_mail": - return ; + return ; case "dhl": - return ; + return ; case "fedex": - return ; + return ; case "ups": - return ; + return ; case "usps": - return ; + return ; default: if (type === "audio") { - return ; + return ; } - return ; + return ; } } @@ -169,11 +172,12 @@ function getVerifiedIcon( label: string, className?: string, type: EventType = "object", + key?: string, ) { const simpleLabel = label.substring(0, label.lastIndexOf("-")); return ( -
+
{getIconForLabel(simpleLabel, type, className)}
@@ -184,11 +188,12 @@ function getRecognizedPlateIcon( label: string, className?: string, type: EventType = "object", + key?: string, ) { const simpleLabel = label.substring(0, label.lastIndexOf("-")); return ( -
+
{getIconForLabel(simpleLabel, type, className)}