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
This commit is contained in:
Josh Hawkins 2026-02-08 07:02:40 -06:00
parent 6accc38275
commit 1fec95f88e
5 changed files with 137 additions and 106 deletions

View File

@ -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

View File

@ -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 = (
<div
className="relative flex w-full cursor-pointer flex-col gap-1.5"
@ -197,20 +204,20 @@ export default function ReviewCard({
</div>
</TooltipTrigger>
<TooltipContent className="smart-capitalize">
{[
...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(),
)}
</TooltipContent>
</Tooltip>
<TimeAgo

View File

@ -371,22 +371,23 @@ export default function LivePlayer({
</TooltipTrigger>
</div>
<TooltipPortal>
<TooltipContent className="smart-capitalize">
<TooltipContent>
{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(),
)}
</TooltipContent>
</TooltipPortal>

View File

@ -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({
</div>
</TooltipTrigger>
</div>
<TooltipContent className="smart-capitalize">
<TooltipContent>
{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(),
)}
</TooltipContent>
</Tooltip>
{!!(

View File

@ -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 <GiPolarBear key={label} className={className} />;
return <GiPolarBear key={iconKey} className={className} />;
case "bicycle":
return <FaBicycle key={label} className={className} />;
return <FaBicycle key={iconKey} className={className} />;
case "bird":
return <PiBirdFill key={label} className={className} />;
return <PiBirdFill key={iconKey} className={className} />;
case "boat":
return <GiSailboat key={label} className={className} />;
return <GiSailboat key={iconKey} className={className} />;
case "bus":
case "school_bus":
return <FaBus key={label} className={className} />;
return <FaBus key={iconKey} className={className} />;
case "car":
case "vehicle":
return <FaCarSide key={label} className={className} />;
return <FaCarSide key={iconKey} className={className} />;
case "cat":
return <FaCat key={label} className={className} />;
return <FaCat key={iconKey} className={className} />;
case "deer":
return <GiDeer key={label} className={className} />;
return <GiDeer key={iconKey} className={className} />;
case "animal":
case "bark":
case "dog":
return <FaDog key={label} className={className} />;
return <FaDog key={iconKey} className={className} />;
case "fox":
return <GiFox key={label} className={className} />;
return <GiFox key={iconKey} className={className} />;
case "goat":
return <GiGoat key={label} className={className} />;
return <GiGoat key={iconKey} className={className} />;
case "horse":
return <FaHorse key={label} className={className} />;
return <FaHorse key={iconKey} className={className} />;
case "kangaroo":
return <GiKangaroo key={label} className={className} />;
return <GiKangaroo key={iconKey} className={className} />;
case "license_plate":
return <LuScanBarcode key={label} className={className} />;
return <LuScanBarcode key={iconKey} className={className} />;
case "motorcycle":
return <FaMotorcycle key={label} className={className} />;
return <FaMotorcycle key={iconKey} className={className} />;
case "mouse":
return <FaMouse key={label} className={className} />;
return <FaMouse key={iconKey} className={className} />;
case "package":
return <LuBox key={label} className={className} />;
return <LuBox key={iconKey} className={className} />;
case "person":
return <BsPersonWalking key={label} className={className} />;
return <BsPersonWalking key={iconKey} className={className} />;
case "rabbit":
return <GiRabbit key={label} className={className} />;
return <GiRabbit key={iconKey} className={className} />;
case "raccoon":
return <GiRaccoonHead key={label} className={className} />;
return <GiRaccoonHead key={iconKey} className={className} />;
case "robot_lawnmower":
return <FaHockeyPuck key={label} className={className} />;
return <FaHockeyPuck key={iconKey} className={className} />;
case "sports_ball":
return <FaFootballBall key={label} className={className} />;
return <FaFootballBall key={iconKey} className={className} />;
case "skunk":
return <GiSquirrel key={label} className={className} />;
return <GiSquirrel key={iconKey} className={className} />;
case "squirrel":
return <LuIcons.LuSquirrel key={label} className={className} />;
return <LuIcons.LuSquirrel key={iconKey} className={className} />;
case "umbrella":
return <FaUmbrella key={label} className={className} />;
return <FaUmbrella key={iconKey} className={className} />;
case "waste_bin":
return <FaRegTrashAlt key={label} className={className} />;
return <FaRegTrashAlt key={iconKey} className={className} />;
// audio
case "crying":
case "laughter":
case "scream":
case "speech":
case "yell":
return <MdRecordVoiceOver key={label} className={className} />;
return <MdRecordVoiceOver key={iconKey} className={className} />;
case "fire_alarm":
return <FaFire key={label} className={className} />;
return <FaFire key={iconKey} className={className} />;
// sub labels
case "amazon":
return <FaAmazon key={label} className={className} />;
return <FaAmazon key={iconKey} className={className} />;
case "an_post":
case "canada_post":
case "dpd":
@ -148,20 +151,20 @@ export function getIconForLabel(
case "postnord":
case "purolator":
case "royal_mail":
return <GiPostStamp key={label} className={className} />;
return <GiPostStamp key={iconKey} className={className} />;
case "dhl":
return <FaDhl key={label} className={className} />;
return <FaDhl key={iconKey} className={className} />;
case "fedex":
return <FaFedex key={label} className={className} />;
return <FaFedex key={iconKey} className={className} />;
case "ups":
return <FaUps key={label} className={className} />;
return <FaUps key={iconKey} className={className} />;
case "usps":
return <FaUsps key={label} className={className} />;
return <FaUsps key={iconKey} className={className} />;
default:
if (type === "audio") {
return <GiSoundWaves key={label} className={className} />;
return <GiSoundWaves key={iconKey} className={className} />;
}
return <LuLassoSelect key={label} className={className} />;
return <LuLassoSelect key={iconKey} className={className} />;
}
}
@ -169,11 +172,12 @@ function getVerifiedIcon(
label: string,
className?: string,
type: EventType = "object",
key?: string,
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={label} className="relative flex items-center">
<div key={key} className="relative flex items-center">
{getIconForLabel(simpleLabel, type, className)}
<FaCheckCircle className="absolute -bottom-0.5 -right-0.5 size-2" />
</div>
@ -184,11 +188,12 @@ function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
key?: string,
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return (
<div key={label} className="relative inline-flex items-center">
<div key={key} className="relative inline-flex items-center">
{getIconForLabel(simpleLabel, type, className)}
<LuScanBarcode className="absolute -bottom-0.5 -right-0.5 size-2" />
</div>