diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 2f5b5bcb1..8b9b39c84 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -91,11 +91,13 @@ "toast": { "success": { "regenerate": "A new description has been requested from {{provider}}. Depending on the speed of your provider, the new description may take some time to regenerate.", - "updatedSublabel": "Successfully updated sub label." + "updatedSublabel": "Successfully updated sub label.", + "updatedLPR": "Successfully updated license plate." }, "error": { "regenerate": "Failed to call {{provider}} for a new description: {{errorMessage}}", - "updatedSublabelFailed": "Failed to update sub label: {{errorMessage}}" + "updatedSublabelFailed": "Failed to update sub label: {{errorMessage}}", + "updatedLPRFailed": "Failed to update license plate: {{errorMessage}}" } } }, @@ -105,10 +107,16 @@ "desc": "Enter a new sub label for this {{label}}", "descNoLabel": "Enter a new sub label for this tracked object" }, + "editLPR": { + "title": "Edit license plate", + "desc": "Enter a new license plate value for this {{label}}", + "descNoLabel": "Enter a new license plate value for this tracked object" + }, "topScore": { "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." }, + "recognizedLicensePlate": "Recognized License Plate", "estimatedSpeed": "Estimated Speed", "objects": "Objects", "camera": "Camera", diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 98b093b8f..f5c19ecb6 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -299,6 +299,7 @@ function ObjectDetailsTab({ const [desc, setDesc] = useState(search?.data.description); const [isSubLabelDialogOpen, setIsSubLabelDialogOpen] = useState(false); + const [isLPRDialogOpen, setIsLPRDialogOpen] = useState(false); const handleDescriptionFocus = useCallback(() => { setInputFocused(true); @@ -557,6 +558,83 @@ function ObjectDetailsTab({ [search, apiHost, mutate, setSearch, t], ); + // recognized plate + + const handleLPRSave = useCallback( + (text: string) => { + if (!search) return; + + // set score to 1.0 if we're manually entering a new plate + const plateScore = text === "" ? undefined : 1.0; + + axios + .post(`${apiHost}api/events/${search.id}/recognized_license_plate`, { + recognizedLicensePlate: text, + recognizedLicensePlateScore: plateScore, + }) + .then((response) => { + if (response.status === 200) { + toast.success(t("details.item.toast.success.updatedLPR"), { + position: "top-center", + }); + + mutate( + (key) => + typeof key === "string" && + (key.includes("events") || + key.includes("events/search") || + key.includes("events/explore")), + (currentData: SearchResult[][] | SearchResult[] | undefined) => { + if (!currentData) return currentData; + return currentData.flat().map((event) => + event.id === search.id + ? { + ...event, + data: { + ...event.data, + recognized_license_plate: text, + recognized_license_plate_score: plateScore, + }, + } + : event, + ); + }, + { + optimisticData: true, + rollbackOnError: true, + revalidate: false, + }, + ); + + setSearch({ + ...search, + data: { + ...search.data, + recognized_license_plate: text, + recognized_license_plate_score: plateScore, + }, + }); + setIsLPRDialogOpen(false); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error( + t("details.item.toast.error.updatedLPRFailed", { + errorMessage, + }), + { + position: "top-center", + }, + ); + }); + }, + [search, apiHost, mutate, setSearch, t], + ); + // face training const hasFace = useMemo(() => { @@ -631,13 +709,30 @@ function ObjectDetailsTab({ {search?.data.recognized_license_plate && (
- Recognized License Plate + {t("details.recognizedLicensePlate")}
{search.data.recognized_license_plate}{" "} {recognizedLicensePlateScore && ` (${recognizedLicensePlateScore}%)`} + + + + { + setIsLPRDialogOpen(true); + }} + /> + + + + + {t("details.editLPR.title")} + + +
@@ -859,7 +954,7 @@ function ObjectDetailsTab({ description={ search.label ? t("details.editSubLabel.desc", { - label: t(search.label, { an: "objects" }), + label: t(search.label, { ns: "objects" }), }) : t("details.editSubLabel.descNoLabel") } @@ -867,6 +962,21 @@ function ObjectDetailsTab({ defaultValue={search?.sub_label || ""} allowEmpty={true} /> +