UI improvements (#19659)

* Add indicator when GenAI review infers suspicious activity

* Fix score filtering logic

* Enable mobile view for classification and optimize for mobile layout

* Add missing keys

* Don't require face rec

* fix key
This commit is contained in:
Nicolas Mowen
2025-08-20 07:28:47 -06:00
committed by GitHub
parent 9fb09408d1
commit 361014f01c
8 changed files with 100 additions and 15 deletions

View File

@@ -34,5 +34,7 @@
"selected_one": "{{count}} selected", "selected_one": "{{count}} selected",
"selected_other": "{{count}} selected", "selected_other": "{{count}} selected",
"camera": "Camera", "camera": "Camera",
"detected": "detected" "detected": "detected",
"suspiciousActivity": "Suspicious Activity",
"threateningActivity": "Threatening Activity"
} }

View File

@@ -132,6 +132,9 @@
"label": "Top Score", "label": "Top Score",
"info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail." "info": "The top score is the highest median score for the tracked object, so this may differ from the score shown on the search result thumbnail."
}, },
"score": {
"label": "Score"
},
"recognizedLicensePlate": "Recognized License Plate", "recognizedLicensePlate": "Recognized License Plate",
"estimatedSpeed": "Estimated Speed", "estimatedSpeed": "Estimated Speed",
"objects": "Objects", "objects": "Objects",
@@ -213,5 +216,11 @@
"error": "Failed to delete tracked object: {{errorMessage}}" "error": "Failed to delete tracked object: {{errorMessage}}"
} }
} }
},
"aiAnalysis": {
"title": "AI Analysis"
},
"concerns": {
"label": "Concerns"
} }
} }

View File

@@ -65,6 +65,7 @@ import { useTranslation } from "react-i18next";
import { supportedLanguageKeys } from "@/lib/const"; import { supportedLanguageKeys } from "@/lib/const";
import { useDocDomain } from "@/hooks/use-doc-domain"; import { useDocDomain } from "@/hooks/use-doc-domain";
import { MdCategory } from "react-icons/md";
type GeneralSettingsProps = { type GeneralSettingsProps = {
className?: string; className?: string;
@@ -315,6 +316,19 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) {
</Link> </Link>
</> </>
)} )}
{isAdmin && isMobile && (
<>
<Link to="/classification">
<MenuItem
className="flex w-full items-center p-2 text-sm"
aria-label={t("menu.classification")}
>
<MdCategory className="mr-2 size-4" />
<span>{t("menu.classification")}</span>
</MenuItem>
</Link>
</>
)}
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}> <DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
{t("menu.appearance")} {t("menu.appearance")}

View File

@@ -86,10 +86,10 @@ export default function ReviewDetailDialog({
let concerns = ""; let concerns = "";
switch (aiAnalysis.potential_threat_level) { switch (aiAnalysis.potential_threat_level) {
case ThreatLevel.SUSPICIOUS: case ThreatLevel.SUSPICIOUS:
concerns = "• Suspicious Activity\n"; concerns = `${t("suspiciousActivity", { ns: "views/events" })}\n`;
break; break;
case ThreatLevel.DANGER: case ThreatLevel.DANGER:
concerns = "• Danger\n"; concerns = `${t("threateningActivity", { ns: "views/events" })}\n`;
break; break;
} }
@@ -98,7 +98,7 @@ export default function ReviewDetailDialog({
}); });
return concerns || "None"; return concerns || "None";
}, [aiAnalysis]); }, [aiAnalysis, t]);
const hasMismatch = useMemo(() => { const hasMismatch = useMemo(() => {
if (!review || !events) { if (!review || !events) {
@@ -270,12 +270,18 @@ export default function ReviewDetailDialog({
isDesktop && "m-2 w-[90%]", isDesktop && "m-2 w-[90%]",
)} )}
> >
AI Analysis {t("aiAnalysis.title")}
<div className="text-sm text-primary/40">Description</div> <div className="text-sm text-primary/40">
{t("details.description.label")}
</div>
<div className="text-sm">{aiAnalysis.scene}</div> <div className="text-sm">{aiAnalysis.scene}</div>
<div className="text-sm text-primary/40">Score</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">{aiAnalysis.confidence * 100}%</div>
<div className="text-sm text-primary/40">Concerns</div> <div className="text-sm text-primary/40">
{t("concerns.label")}
</div>
<div className="text-sm">{aiThreatLevel}</div> <div className="text-sm">{aiThreatLevel}</div>
</div> </div>
)} )}

View File

@@ -60,7 +60,7 @@ export default function TrainFilterDialog({
moreFiltersSelected ? "text-white" : "text-secondary-foreground", moreFiltersSelected ? "text-white" : "text-secondary-foreground",
)} )}
/> />
{t("more")} {isDesktop && t("more")}
</Button> </Button>
); );
const content = ( const content = (

View File

@@ -22,6 +22,8 @@ import { InProgressPreview, VideoPreview } from "../preview/ScrubbablePreview";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { baseUrl } from "@/api/baseUrl"; import { baseUrl } from "@/api/baseUrl";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FaExclamationTriangle } from "react-icons/fa";
import { MdOutlinePersonSearch } from "react-icons/md";
type PreviewPlayerProps = { type PreviewPlayerProps = {
review: ReviewSegment; review: ReviewSegment;
@@ -234,7 +236,12 @@ export default function PreviewThumbnailPlayer({
)} )}
/> />
)} )}
<div className={cn("absolute left-0 top-2", !isSafari && "z-40")}> <div
className={cn(
"absolute left-0 top-2 flex gap-2",
!isSafari && "z-40",
)}
>
<Tooltip> <Tooltip>
<div <div
className="flex" className="flex"
@@ -242,7 +249,7 @@ export default function PreviewThumbnailPlayer({
onMouseLeave={() => setTooltipHovering(false)} onMouseLeave={() => setTooltipHovering(false)}
> >
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div className="mx-3 pb-1 text-sm text-white"> <div className="ml-3 pb-1 text-sm text-white">
{(review.severity == "alert" || {(review.severity == "alert" ||
review.severity == "detection") && ( review.severity == "detection") && (
<> <>
@@ -279,6 +286,45 @@ export default function PreviewThumbnailPlayer({
.replaceAll("-verified", "")} .replaceAll("-verified", "")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
{!!(
review.data.metadata?.potential_threat_level &&
!review.has_been_reviewed
) && (
<Tooltip>
<div
className="flex"
onMouseEnter={() => setTooltipHovering(true)}
onMouseLeave={() => setTooltipHovering(false)}
>
<TooltipTrigger asChild>
<div className="pb-1 text-sm text-white">
{(review.severity == "alert" ||
review.severity == "detection") && (
<>
<Chip
className={`flex items-start justify-between space-x-1 ${playingBack ? "hidden" : ""} z-0 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500`}
onClick={() => onClick(review, false, true)}
>
{review.data.metadata.potential_threat_level == 1 ? (
<MdOutlinePersonSearch className="size-3" />
) : (
<FaExclamationTriangle className="size-3" />
)}
</Chip>
</>
)}
</div>
</TooltipTrigger>
</div>
<TooltipContent className="smart-capitalize">
{review.data.metadata.potential_threat_level == 1 ? (
<>{t("suspiciousActivity", { ns: "views/events" })}</>
) : (
<>{t("threateningActivity", { ns: "views/events" })}</>
)}
</TooltipContent>
</Tooltip>
)}
</div> </div>
{!playingBack && ( {!playingBack && (
<div <div

View File

@@ -79,7 +79,9 @@ function ModelCard({ config, onClick }: ModelCardProps) {
)} )}
onClick={() => onClick()} onClick={() => onClick()}
> >
<div className="grid size-48 grid-cols-2 gap-2"> <div
className={cn("grid size-48 grid-cols-2 gap-2", isMobile && "w-full")}
>
{Object.entries(coverImages).map(([key, image]) => ( {Object.entries(coverImages).map(([key, image]) => (
<img <img
key={key} key={key}

View File

@@ -362,7 +362,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
) : ( ) : (
<MdAutoFixHigh className="text-secondary-foreground" /> <MdAutoFixHigh className="text-secondary-foreground" />
)} )}
{t("button.trainModel")} {isDesktop && t("button.trainModel")}
</Button> </Button>
</div> </div>
)} )}
@@ -713,7 +713,7 @@ function TrainGrid({
if ( if (
trainFilter.max_score && trainFilter.max_score &&
trainFilter.max_score <= data.score / 100.0 trainFilter.max_score < data.score / 100.0
) { ) {
return false; return false;
} }
@@ -725,7 +725,12 @@ function TrainGrid({
); );
return ( return (
<div className="flex flex-wrap gap-2 overflow-y-auto p-2"> <div
className={cn(
"flex flex-wrap gap-2 overflow-y-auto p-2",
isMobile && "justify-center",
)}
>
{trainData?.map((data) => ( {trainData?.map((data) => (
<div <div
key={data.timestamp} key={data.timestamp}
@@ -734,6 +739,7 @@ function TrainGrid({
selectedImages.includes(data.raw) selectedImages.includes(data.raw)
? "shadow-selected outline-selected" ? "shadow-selected outline-selected"
: "outline-transparent duration-500", : "outline-transparent duration-500",
isMobile && "w-[48%]",
)} )}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();