This commit is contained in:
Josh Hawkins 2025-03-12 17:26:27 -05:00
parent 3dcef62271
commit 7e4af87c1c
5 changed files with 99 additions and 65 deletions

View File

@ -333,13 +333,18 @@ function ObjectDetailsTab({
}
}, [search]);
const identifierScore = useMemo(() => {
const recognizedLicensePlateScore = useMemo(() => {
if (!search) {
return undefined;
}
if (search.data.identifier && search.data?.identifier_score) {
return Math.round((search.data?.identifier_score ?? 0) * 100);
if (
search.data.recognized_license_plate &&
search.data?.recognized_license_plate_score
) {
return Math.round(
(search.data?.recognized_license_plate_score ?? 0) * 100,
);
} else {
return undefined;
}
@ -550,13 +555,16 @@ function ObjectDetailsTab({
</Tooltip>
</div>
</div>
{search?.data.identifier && (
{search?.data.recognized_license_plate && (
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Identifier</div>
<div className="text-sm text-primary/40">
Recognized License Plate
</div>
<div className="flex flex-col space-y-0.5 text-sm">
<div className="flex flex-row items-center gap-2">
{search.data.identifier}{" "}
{identifierScore && ` (${identifierScore}%)`}
{search.data.recognized_license_plate}{" "}
{recognizedLicensePlateScore &&
` (${recognizedLicensePlateScore}%)`}
</div>
</div>
</div>

View File

@ -86,7 +86,7 @@ export default function SearchFilterDialog({
(currentFilter.max_speed ?? 150) < 150 ||
(currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0 ||
(currentFilter.identifier?.length ?? 0) > 0),
(currentFilter.recognized_license_plate?.length ?? 0) > 0),
[currentFilter],
);
@ -128,10 +128,13 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
}
/>
<IdentifierFilterContent
identifiers={currentFilter.identifier}
setIdentifiers={(identifiers) =>
setCurrentFilter({ ...currentFilter, identifier: identifiers })
<RecognizedLicensePlatesFilterContent
recognizedLicensePlates={currentFilter.recognized_license_plate}
setRecognizedLicensePlates={(plate) =>
setCurrentFilter({
...currentFilter,
recognized_license_plate: plate,
})
}
/>
<ScoreFilterContent
@ -207,7 +210,7 @@ export default function SearchFilterDialog({
max_speed: undefined,
has_snapshot: undefined,
has_clip: undefined,
identifier: undefined,
recognized_license_plate: undefined,
}));
}}
>
@ -847,97 +850,109 @@ export function SnapshotClipFilterContent({
);
}
type IdentifierFilterContentProps = {
identifiers: string[] | undefined;
setIdentifiers: (identifiers: string[] | undefined) => void;
type RecognizedLicensePlatesFilterContentProps = {
recognizedLicensePlates: string[] | undefined;
setRecognizedLicensePlates: (
recognizedLicensePlates: string[] | undefined,
) => void;
};
export function IdentifierFilterContent({
identifiers,
setIdentifiers,
}: IdentifierFilterContentProps) {
const { data: allIdentifiers, error } = useSWR<string[]>("identifiers", {
revalidateOnFocus: false,
});
const [selectedIdentifiers, setSelectedIdentifiers] = useState<string[]>(
identifiers || [],
export function RecognizedLicensePlatesFilterContent({
recognizedLicensePlates,
setRecognizedLicensePlates,
}: RecognizedLicensePlatesFilterContentProps) {
const { data: allRecognizedLicensePlates, error } = useSWR<string[]>(
"recognized_license_plates",
{
revalidateOnFocus: false,
},
);
const [selectedRecognizedLicensePlates, setSelectedRecognizedLicensePlates] =
useState<string[]>(recognizedLicensePlates || []);
const [inputValue, setInputValue] = useState("");
useEffect(() => {
if (identifiers) {
setSelectedIdentifiers(identifiers);
if (recognizedLicensePlates) {
setSelectedRecognizedLicensePlates(recognizedLicensePlates);
} else {
setSelectedIdentifiers([]);
setSelectedRecognizedLicensePlates([]);
}
}, [identifiers]);
}, [recognizedLicensePlates]);
const handleSelect = (identifier: string) => {
const newSelected = selectedIdentifiers.includes(identifier)
? selectedIdentifiers.filter((id) => id !== identifier) // Deselect
: [...selectedIdentifiers, identifier]; // Select
const handleSelect = (recognizedLicensePlate: string) => {
const newSelected = selectedRecognizedLicensePlates.includes(
recognizedLicensePlate,
)
? selectedRecognizedLicensePlates.filter(
(id) => id !== recognizedLicensePlate,
) // Deselect
: [...selectedRecognizedLicensePlates, recognizedLicensePlate]; // Select
setSelectedIdentifiers(newSelected);
setSelectedRecognizedLicensePlates(newSelected);
if (newSelected.length === 0) {
setIdentifiers(undefined); // Clear filter if no identifiers selected
setRecognizedLicensePlates(undefined); // Clear filter if no plates selected
} else {
setIdentifiers(newSelected);
setRecognizedLicensePlates(newSelected);
}
};
if (!allIdentifiers || allIdentifiers.length === 0) {
if (!allRecognizedLicensePlates || allRecognizedLicensePlates.length === 0) {
return null;
}
const filteredIdentifiers =
allIdentifiers?.filter((id) =>
const filteredRecognizedLicensePlates =
allRecognizedLicensePlates?.filter((id) =>
id.toLowerCase().includes(inputValue.toLowerCase()),
) || [];
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">Identifiers</div>
<div className="mb-3 text-lg">Recognized License Plates</div>
{error ? (
<p className="text-sm text-red-500">Failed to load identifiers</p>
) : !allIdentifiers ? (
<p className="text-sm text-muted-foreground">Loading identifiers...</p>
<p className="text-sm text-red-500">
Failed to load recognized license plates.
</p>
) : !allRecognizedLicensePlates ? (
<p className="text-sm text-muted-foreground">
Loading recognized license plates...
</p>
) : (
<>
<Command className="border border-input bg-background">
<CommandInput
placeholder="Type to search identifiers..."
placeholder="Type to search license plates..."
value={inputValue}
onValueChange={setInputValue}
/>
<CommandList className="max-h-[200px] overflow-auto">
{filteredIdentifiers.length === 0 && inputValue && (
<CommandEmpty>No identifiers found.</CommandEmpty>
{filteredRecognizedLicensePlates.length === 0 && inputValue && (
<CommandEmpty>No license plates found.</CommandEmpty>
)}
{filteredIdentifiers.map((identifier) => (
{filteredRecognizedLicensePlates.map((plate) => (
<CommandItem
key={identifier}
value={identifier}
onSelect={() => handleSelect(identifier)}
key={plate}
value={plate}
onSelect={() => handleSelect(plate)}
className="cursor-pointer"
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
selectedIdentifiers.includes(identifier)
selectedRecognizedLicensePlates.includes(plate)
? "opacity-100"
: "opacity-0",
)}
/>
{identifier}
{plate}
</CommandItem>
))}
</CommandList>
</Command>
{selectedIdentifiers.length > 0 && (
{selectedRecognizedLicensePlates.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{selectedIdentifiers.map((id) => (
{selectedRecognizedLicensePlates.map((id) => (
<span
key={id}
className="inline-flex items-center rounded bg-selected px-2 py-1 text-sm text-white"
@ -956,7 +971,7 @@ export function IdentifierFilterContent({
</>
)}
<p className="mt-1 text-sm text-muted-foreground">
Select one or more identifiers from the list.
Select one or more plates from the list.
</p>
</div>
);

View File

@ -105,7 +105,8 @@ export default function Explore() {
cameras: searchSearchParams["cameras"],
labels: searchSearchParams["labels"],
sub_labels: searchSearchParams["sub_labels"],
identifier: searchSearchParams["identifier"],
recognized_license_plate:
searchSearchParams["recognized_license_plate"],
zones: searchSearchParams["zones"],
before: searchSearchParams["before"],
after: searchSearchParams["after"],
@ -141,7 +142,8 @@ export default function Explore() {
cameras: searchSearchParams["cameras"],
labels: searchSearchParams["labels"],
sub_labels: searchSearchParams["sub_labels"],
identifier: searchSearchParams["identifier"],
recognized_license_plate:
searchSearchParams["recognized_license_plate"],
zones: searchSearchParams["zones"],
before: searchSearchParams["before"],
after: searchSearchParams["after"],

View File

@ -58,8 +58,8 @@ export type SearchResult = {
average_estimated_speed: number;
velocity_angle: number;
path_data: [number[], number][];
identifier?: string;
identifier_score?: number;
recognized_license_plate?: string;
recognized_license_plate_score?: number;
};
};
@ -68,7 +68,7 @@ export type SearchFilter = {
cameras?: string[];
labels?: string[];
sub_labels?: string[];
identifier?: string[];
recognized_license_plate?: string[];
zones?: string[];
before?: number;
after?: number;
@ -92,7 +92,7 @@ export type SearchQueryParams = {
cameras?: string[];
labels?: string[];
sub_labels?: string[];
identifier?: string[];
recognized_license_plate?: string[];
zones?: string[];
before?: string;
after?: string;

View File

@ -121,7 +121,9 @@ export default function SearchView({
}, [config, searchFilter]);
const { data: allSubLabels } = useSWR("sub_labels");
const { data: allIdentifiers } = useSWR("identifiers");
const { data: allRecognizedLicensePlates } = useSWR(
"recognized_license_plate",
);
const allZones = useMemo<string[]>(() => {
if (!config) {
@ -161,13 +163,20 @@ export default function SearchView({
max_score: ["100"],
min_speed: ["1"],
max_speed: ["150"],
identifier: allIdentifiers,
recognized_license_plate: allRecognizedLicensePlates,
has_clip: ["yes", "no"],
has_snapshot: ["yes", "no"],
...(config?.plus?.enabled &&
searchFilter?.has_snapshot && { is_submitted: ["yes", "no"] }),
}),
[config, allLabels, allZones, allSubLabels, allIdentifiers, searchFilter],
[
config,
allLabels,
allZones,
allSubLabels,
allRecognizedLicensePlates,
searchFilter,
],
);
// remove duplicate event ids