add triggers from explore

This commit is contained in:
Josh Hawkins 2025-07-02 17:19:02 -05:00
parent aca9e15790
commit efe5864ebc
9 changed files with 84 additions and 22 deletions

View File

@ -175,6 +175,10 @@
"label": "Find similar",
"aria": "Find similar tracked objects"
},
"addTrigger": {
"label": "Add trigger",
"aria": "Add a trigger for this tracked object"
},
"audioTranscription": {
"label": "Transcribe",
"aria": "Request audio transcription"

View File

@ -673,15 +673,15 @@
"dialog": {
"createTrigger": {
"title": "Create Trigger",
"desc": "Create a new trigger for camera '{{camera}}'."
"desc": "Create a trigger for camera {{camera}}"
},
"editTrigger": {
"title": "Edit Trigger",
"desc": "Edit the settings for an existing trigger on camera '{{camera}}'."
"desc": "Edit the settings for trigger on camera {{camera}}"
},
"deleteTrigger": {
"title": "Delete Trigger",
"desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong> from camera '{{camera}}'? This action cannot be undone."
"desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong> from camera {{camera}}? This action cannot be undone."
},
"form": {
"name": {
@ -725,9 +725,9 @@
},
"toast": {
"success": {
"createTrigger": "Trigger '{{name}}' created successfully.",
"updateTrigger": "Trigger '{{name}}' updated successfully.",
"deleteTrigger": "Trigger '{{name}}' deleted successfully."
"createTrigger": "Trigger {{name}} created successfully.",
"updateTrigger": "Trigger {{name}} updated successfully.",
"deleteTrigger": "Trigger {{name}} deleted successfully."
},
"error": {
"createTriggerFailed": "Failed to create trigger: {{errorMessage}}",

View File

@ -15,6 +15,7 @@ type SearchThumbnailProps = {
refreshResults: () => void;
showObjectLifecycle: () => void;
showSnapshot: () => void;
addTrigger: () => void;
};
export default function SearchThumbnailFooter({
@ -24,6 +25,7 @@ export default function SearchThumbnailFooter({
refreshResults,
showObjectLifecycle,
showSnapshot,
addTrigger,
}: SearchThumbnailProps) {
const { t } = useTranslation(["views/search"]);
const { data: config } = useSWR<FrigateConfig>("config");
@ -61,6 +63,7 @@ export default function SearchThumbnailFooter({
refreshResults={refreshResults}
showObjectLifecycle={showObjectLifecycle}
showSnapshot={showSnapshot}
addTrigger={addTrigger}
/>
</div>
</div>

View File

@ -41,6 +41,7 @@ import {
import useSWR from "swr";
import { Trans, useTranslation } from "react-i18next";
import { BsFillLightningFill } from "react-icons/bs";
type SearchResultActionsProps = {
searchResult: SearchResult;
@ -48,6 +49,7 @@ type SearchResultActionsProps = {
refreshResults: () => void;
showObjectLifecycle: () => void;
showSnapshot: () => void;
addTrigger: () => void;
isContextMenu?: boolean;
children?: ReactNode;
};
@ -58,6 +60,7 @@ export default function SearchResultActions({
refreshResults,
showObjectLifecycle,
showSnapshot,
addTrigger,
isContextMenu = false,
children,
}: SearchResultActionsProps) {
@ -138,6 +141,16 @@ export default function SearchResultActions({
<span>{t("itemMenu.findSimilar.label")}</span>
</MenuItem>
)}
{config?.semantic_search?.enabled &&
searchResult.data.type == "object" && (
<MenuItem
aria-label={t("itemMenu.addTrigger.aria")}
onClick={addTrigger}
>
<BsFillLightningFill className="mr-2 size-4" />
<span>{t("itemMenu.addTrigger.label")}</span>
</MenuItem>
)}
{isMobileOnly &&
config?.plus?.enabled &&
searchResult.has_snapshot &&

View File

@ -95,9 +95,7 @@ export default function CreateTriggerDialog({
.number()
.min(0, t("triggers.dialog.form.threshold.error.min"))
.max(1, t("triggers.dialog.form.threshold.error.max")),
actions: z
.array(z.enum(["alert", "notification"]))
.min(1, t("triggers.dialog.form.actions.error.min")),
actions: z.array(z.enum(["alert", "notification"])),
});
const form = useForm<z.infer<typeof formSchema>>({
@ -109,7 +107,7 @@ export default function CreateTriggerDialog({
type: trigger?.type ?? "description",
data: trigger?.data ?? "",
threshold: trigger?.threshold ?? 0.5,
actions: trigger?.actions ?? ["alert"],
actions: trigger?.actions ?? [],
},
});
@ -138,17 +136,22 @@ export default function CreateTriggerDialog({
type: "description",
data: "",
threshold: 0.5,
actions: ["alert"],
actions: [],
});
} else if (trigger) {
form.reset({
enabled: trigger.enabled,
name: trigger.name,
type: trigger.type,
data: trigger.data,
threshold: trigger.threshold,
actions: trigger.actions,
});
form.reset(
{
enabled: trigger.enabled,
name: trigger.name,
type: trigger.type,
data: trigger.data,
threshold: trigger.threshold,
actions: trigger.actions,
},
{ keepDirty: false, keepTouched: false }, // Reset validation state
);
// Trigger validation to ensure isValid updates
// form.trigger();
}
}, [show, trigger, form]);

View File

@ -173,7 +173,7 @@ export default function Settings() {
}
}
// don't clear url params if we're creating a new object mask
return !searchParams.has("object_mask");
return !(searchParams.has("object_mask") || searchParams.has("event_id"));
});
useSearchEffect("camera", (camera: string) => {
@ -181,8 +181,8 @@ export default function Settings() {
if (cameraNames.includes(camera)) {
setSelectedCamera(camera);
}
// don't clear url params if we're creating a new object mask
return !searchParams.has("object_mask");
// don't clear url params if we're creating a new object mask or trigger
return !(searchParams.has("object_mask") || searchParams.has("event_id"));
});
useEffect(() => {

View File

@ -218,6 +218,7 @@ function ExploreThumbnailImage({
const apiHost = useApiHost();
const { data: config } = useSWR<FrigateConfig>("config");
const [imgRef, imgLoaded, onImgLoad] = useImageLoaded();
const navigate = useNavigate();
const handleFindSimilar = () => {
if (config?.semantic_search.enabled) {
@ -233,6 +234,12 @@ function ExploreThumbnailImage({
onSelectSearch(event, false, "snapshot");
};
const handleAddTrigger = () => {
navigate(
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
);
};
return (
<SearchResultActions
searchResult={event}
@ -240,6 +247,7 @@ function ExploreThumbnailImage({
refreshResults={mutate}
showObjectLifecycle={handleShowObjectLifecycle}
showSnapshot={handleShowSnapshot}
addTrigger={handleAddTrigger}
isContextMenu={true}
>
<div className="relative size-full">

View File

@ -32,6 +32,7 @@ import Chip from "@/components/indicators/Chip";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import SearchActionGroup from "@/components/filter/SearchActionGroup";
import { Trans, useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
type SearchViewProps = {
search: string;
@ -76,6 +77,7 @@ export default function SearchView({
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const navigate = useNavigate();
// grid
@ -648,6 +650,16 @@ export default function SearchView({
showSnapshot={() =>
onSelectSearch(value, false, "snapshot")
}
addTrigger={() => {
if (
config?.semantic_search.enabled &&
value.data.type == "object"
) {
navigate(
`/settings?page=triggers&camera=${value.camera}&event_id=${value.id}`,
);
}
}}
/>
</div>
</div>

View File

@ -26,6 +26,7 @@ import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog";
import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog";
import { FrigateConfig } from "@/types/frigateConfig";
import { Trigger, TriggerAction, TriggerType } from "@/types/trigger";
import { useSearchEffect } from "@/hooks/use-overlay-state";
type ConfigSetBody = {
requires_restart: number;
@ -296,6 +297,24 @@ export default function TriggerView({
}
}, [selectedCamera, setUnsavedChanges]);
// for adding a trigger with event id via explore context menu
useSearchEffect("event_id", (eventId: string) => {
if (!config || isLoading) {
return false;
}
setShowCreate(true);
setSelectedTrigger({
enabled: true,
name: "",
type: "thumbnail",
data: eventId,
threshold: 0.5,
actions: [],
});
return true;
});
if (!config || !selectedCamera) {
return (
<div className="flex h-full w-full items-center justify-center">