mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-09-26 19:41:29 +08:00
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:
@@ -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"
|
||||||
}
|
}
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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")}
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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 = (
|
||||||
|
@@ -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
|
||||||
|
@@ -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}
|
||||||
|
@@ -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();
|
||||||
|
Reference in New Issue
Block a user