Compare commits

...
Author SHA1 Message Date
Josh Hawkins 35cca5bd4b fix title classes on desktop 2025-11-07 07:48:22 -06:00
Josh Hawkins b93dcf151d add optional ref to always scroll to top
the more filters in explore was not scrolled to the top on open due to the use of framer motion
2025-11-07 07:45:07 -06:00
Nicolas Mowen b1eca87cb5 Skip azure execution provider 2025-11-07 06:42:33 -07:00
Josh Hawkins 4ab9fd114f Use a column layout for MobilePageContent in PlatformAwareSheet
This is so the header is outside the scrolling area and the content can grow/scroll independently. This now matches the way it's done in classification
2025-11-07 07:09:12 -06:00
Josh Hawkins ab43a33d19 move find similar to actions menu 2025-11-06 21:24:11 -06:00
Josh Hawkins f3c0c8b61a add trigger to detail actions menu 2025-11-06 18:58:22 -06:00
Josh Hawkins 19e36fc56b ensure name is valid for search effect trigger 2025-11-06 18:54:18 -06:00
Josh Hawkins 6258b90e91 prevent x overflow in detail stream on mobile safari 2025-11-06 18:46:02 -06:00
Josh Hawkins 1d94b24cfe hide x icon on restart sheet to prevent closure issues 2025-11-06 18:02:56 -06:00
Josh Hawkins 294e04adb1 spacing 2025-11-06 17:53:43 -06:00
Josh Hawkins d4af7f0966 fix audio transcription embedding 2025-11-06 17:38:34 -06:00
Nicolas Mowen 8ddf3ac72e Only show the genai summary popup on mobile when timeline is open 2025-11-06 16:15:38 -07:00
Nicolas Mowen 5b7fe10caf Fix dialog getting stuck 2025-11-06 16:03:36 -07:00
Nicolas Mowen d8e32b8724 Ensure after creating a class that things are correct 2025-11-06 15:57:42 -07:00
Nicolas Mowen ca91d3d82b Fix deletion of classification images and library 2025-11-06 15:55:05 -07:00
Josh Hawkins 88b6bd7535 tweak spacing in annotation settings popover 2025-11-06 16:29:27 -06:00
Josh Hawkins 9f73145d63 don't show submit to plus for non-objects and if plus is disabled 2025-11-06 16:25:50 -06:00
Josh Hawkins f2e3579030 pointer cursor on event menu items in detail stream 2025-11-06 16:22:35 -06:00
Josh Hawkins 796a12bf10 add margin 2025-11-06 16:16:41 -06:00
Josh Hawkins aa6cd1ec05 remove frigate+ icon from explore grid footer 2025-11-06 16:16:02 -06:00
22 changed files with 249 additions and 237 deletions
+6 -3
View File
@@ -662,8 +662,11 @@ def delete_classification_dataset_images(
if os.path.isfile(file_path):
os.unlink(file_path)
if os.path.exists(folder) and not os.listdir(folder):
os.rmdir(folder)
return JSONResponse(
content=({"success": True, "message": "Successfully deleted faces."}),
content=({"success": True, "message": "Successfully deleted images."}),
status_code=200,
)
@@ -723,7 +726,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
os.unlink(training_file)
return JSONResponse(
content=({"success": True, "message": "Successfully deleted faces."}),
content=({"success": True, "message": "Successfully categorized image."}),
status_code=200,
)
@@ -761,7 +764,7 @@ def delete_classification_train_images(request: Request, name: str, body: dict =
os.unlink(file_path)
return JSONResponse(
content=({"success": True, "message": "Successfully deleted faces."}),
content=({"success": True, "message": "Successfully deleted images."}),
status_code=200,
)
@@ -9,7 +9,6 @@ from typing import Optional
from faster_whisper import WhisperModel
from peewee import DoesNotExist
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig
from frigate.const import (
@@ -32,11 +31,13 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
self,
config: FrigateConfig,
requestor: InterProcessRequestor,
embeddings,
metrics: DataProcessorMetrics,
):
super().__init__(config, metrics, None)
self.config = config
self.requestor = requestor
self.embeddings = embeddings
self.recognizer = None
self.transcription_lock = threading.Lock()
self.transcription_thread = None
@@ -128,10 +129,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
)
# Embed the description
self.requestor.send_data(
EmbeddingsRequestEnum.embed_description.value,
{"id": event_id, "description": transcription},
)
self.embeddings.embed_description(event_id, transcription)
except DoesNotExist:
logger.debug("No recording found for audio transcription post-processing")
+3 -1
View File
@@ -226,7 +226,9 @@ class EmbeddingMaintainer(threading.Thread):
for c in self.config.cameras.values()
):
self.post_processors.append(
AudioTranscriptionPostProcessor(self.config, self.requestor, metrics)
AudioTranscriptionPostProcessor(
self.config, self.requestor, self.embeddings, metrics
)
)
semantic_trigger_processor: SemanticTriggerProcessor | None = None
+4
View File
@@ -369,6 +369,10 @@ def get_ort_providers(
"enable_cpu_mem_arena": False,
}
)
elif provider == "AzureExecutionProvider":
# Skip Azure provider - not typically available on local hardware
# and prevents fallback to OpenVINO when it's the first provider
continue
else:
providers.append(provider)
options.append({})
@@ -14,7 +14,6 @@ type SearchThumbnailProps = {
findSimilar: () => void;
refreshResults: () => void;
showTrackingDetails: () => void;
showSnapshot: () => void;
addTrigger: () => void;
};
@@ -24,7 +23,6 @@ export default function SearchThumbnailFooter({
findSimilar,
refreshResults,
showTrackingDetails,
showSnapshot,
addTrigger,
}: SearchThumbnailProps) {
const { t } = useTranslation(["views/search"]);
@@ -62,7 +60,6 @@ export default function SearchThumbnailFooter({
findSimilar={findSimilar}
refreshResults={refreshResults}
showTrackingDetails={showTrackingDetails}
showSnapshot={showSnapshot}
addTrigger={addTrigger}
/>
</div>
@@ -6,10 +6,7 @@ import { toast } from "sonner";
import axios from "axios";
import { LuCamera, LuDownload, LuTrash2 } from "react-icons/lu";
import { FiMoreVertical } from "react-icons/fi";
import { FaArrowsRotate } from "react-icons/fa6";
import { MdImageSearch } from "react-icons/md";
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
import { isMobileOnly } from "react-device-detect";
import { buttonVariants } from "@/components/ui/button";
import {
ContextMenu,
@@ -33,23 +30,18 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import useSWR from "swr";
import { Trans, useTranslation } from "react-i18next";
import { BsFillLightningFill } from "react-icons/bs";
import BlurredIconButton from "../button/BlurredIconButton";
import { PiPath } from "react-icons/pi";
type SearchResultActionsProps = {
searchResult: SearchResult;
findSimilar: () => void;
refreshResults: () => void;
showTrackingDetails: () => void;
showSnapshot: () => void;
addTrigger: () => void;
isContextMenu?: boolean;
children?: ReactNode;
@@ -60,7 +52,6 @@ export default function SearchResultActions({
findSimilar,
refreshResults,
showTrackingDetails,
showSnapshot,
addTrigger,
isContextMenu = false,
children,
@@ -129,7 +120,7 @@ export default function SearchResultActions({
aria-label={t("itemMenu.viewTrackingDetails.aria")}
onClick={showTrackingDetails}
>
<FaArrowsRotate className="mr-2 size-4" />
<PiPath className="mr-2 size-4" />
<span>{t("itemMenu.viewTrackingDetails.label")}</span>
</MenuItem>
)}
@@ -152,18 +143,14 @@ export default function SearchResultActions({
<span>{t("itemMenu.addTrigger.label")}</span>
</MenuItem>
)}
{isMobileOnly &&
config?.plus?.enabled &&
searchResult.has_snapshot &&
searchResult.end_time &&
searchResult.data.type == "object" &&
!searchResult.plus_id && (
{config?.semantic_search?.enabled &&
searchResult.data.type == "object" && (
<MenuItem
aria-label={t("itemMenu.submitToPlus.aria")}
onClick={showSnapshot}
aria-label={t("itemMenu.findSimilar.aria")}
onClick={findSimilar}
>
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
<span>{t("itemMenu.submitToPlus.label")}</span>
<MdImageSearch className="mr-2 size-4" />
<span>{t("itemMenu.findSimilar.label")}</span>
</MenuItem>
)}
<MenuItem
@@ -211,44 +198,6 @@ export default function SearchResultActions({
</ContextMenu>
) : (
<>
{config?.semantic_search?.enabled &&
searchResult.data.type == "object" && (
<Tooltip>
<TooltipTrigger asChild>
<BlurredIconButton
onClick={findSimilar}
aria-label={t("itemMenu.findSimilar.aria")}
>
<MdImageSearch className="size-5" />
</BlurredIconButton>
</TooltipTrigger>
<TooltipContent>
{t("itemMenu.findSimilar.label")}
</TooltipContent>
</Tooltip>
)}
{!isMobileOnly &&
config?.plus?.enabled &&
searchResult.has_snapshot &&
searchResult.end_time &&
searchResult.data.type == "object" &&
!searchResult.plus_id && (
<Tooltip>
<TooltipTrigger asChild>
<BlurredIconButton
onClick={showSnapshot}
aria-label={t("itemMenu.submitToPlus.aria")}
>
<FrigatePlusIcon className="size-5" />
</BlurredIconButton>
</TooltipTrigger>
<TooltipContent>
{t("itemMenu.submitToPlus.label")}
</TooltipContent>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
+17 -1
View File
@@ -4,6 +4,7 @@ import {
useEffect,
useState,
useCallback,
useRef,
} from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
@@ -121,17 +122,20 @@ export function MobilePagePortal({
type MobilePageContentProps = {
children: React.ReactNode;
className?: string;
scrollerRef?: React.RefObject<HTMLDivElement>;
};
export function MobilePageContent({
children,
className,
scrollerRef,
}: MobilePageContentProps) {
const context = useContext(MobilePageContext);
if (!context)
throw new Error("MobilePageContent must be used within MobilePage");
const [isVisible, setIsVisible] = useState(context.open);
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (context.open) {
@@ -140,15 +144,27 @@ export function MobilePageContent({
}, [context.open]);
const handleAnimationComplete = () => {
if (!context.open) {
if (context.open) {
// After opening animation completes, ensure scroller is at the top
if (scrollerRef?.current) {
scrollerRef.current.scrollTop = 0;
}
} else {
setIsVisible(false);
}
};
useEffect(() => {
if (context.open && scrollerRef?.current) {
scrollerRef.current.scrollTop = 0;
}
}, [context.open, scrollerRef]);
return (
<AnimatePresence>
{isVisible && (
<motion.div
ref={containerRef}
className={cn(
"fixed inset-0 z-50 mb-12 bg-background",
isPWA && "mb-16",
@@ -97,14 +97,12 @@ export default function ClassificationSelectionDialog({
return (
<div className={className ?? "flex"}>
{newClass && (
<TextEntryDialog
open={true}
setOpen={setNewClass}
title={t("createCategory.new")}
onSave={(newCat) => onCategorizeImage(newCat)}
/>
)}
<TextEntryDialog
open={newClass}
setOpen={setNewClass}
title={t("createCategory.new")}
onSave={(newCat) => onCategorizeImage(newCat)}
/>
<Tooltip>
<Selector>
@@ -141,50 +141,52 @@ export function AnnotationSettingsPane({
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-1 flex-col space-y-6"
className="flex flex-1 flex-col space-y-3"
>
<FormField
control={form.control}
name="annotationOffset"
render={({ field }) => (
<FormItem className="flex flex-row items-start justify-between space-x-2">
<div className="flex flex-col gap-1">
<FormLabel>
{t("trackingDetails.annotationSettings.offset.label")}
</FormLabel>
<FormDescription>
<Trans ns="views/explore">
trackingDetails.annotationSettings.offset.millisecondsToOffset
</Trans>
<FormMessage />
<div className="mt-2">
{t("trackingDetails.annotationSettings.offset.tips")}
<div className="mt-2 flex items-center text-primary">
<Link
to={getLocaleDocUrl("configuration/reference")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
<>
<FormItem className="flex flex-row items-start justify-between space-x-2">
<div className="flex flex-col gap-1">
<FormLabel>
{t("trackingDetails.annotationSettings.offset.label")}
</FormLabel>
<FormDescription>
<Trans ns="views/explore">
trackingDetails.annotationSettings.offset.millisecondsToOffset
</Trans>
<FormMessage />
</FormDescription>
</div>
<div className="flex flex-col gap-3">
<div className="min-w-24">
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 text-center hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0"
{...field}
/>
</FormControl>
</div>
</FormDescription>
</div>
<div className="flex flex-col gap-3">
<div className="min-w-24">
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 text-center hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder="0"
{...field}
/>
</FormControl>
</div>
</FormItem>
<div className="mt-1 text-sm text-secondary-foreground">
{t("trackingDetails.annotationSettings.offset.tips")}
<div className="mt-2 flex items-center text-primary-variant">
<Link
to={getLocaleDocUrl("configuration/reference")}
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</div>
</FormItem>
</>
)}
/>
@@ -111,6 +111,23 @@ export default function DetailActionsMenu({
</div>
</DropdownMenuItem>
)}
{config?.semantic_search.enabled && search.data.type == "object" && (
<DropdownMenuItem
onClick={() => {
setIsOpen(false);
setTimeout(() => {
navigate(
`/settings?page=triggers&camera=${search.camera}&event_id=${search.id}`,
);
}, 0);
}}
>
<div className="flex cursor-pointer items-center gap-2">
<span>{t("itemMenu.addTrigger.label")}</span>
</div>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
@@ -1242,106 +1242,110 @@ function ObjectDetailsTab({
</div>
</div>
<div
className={cn(
"my-2 flex w-full flex-col justify-between gap-1.5",
state == "submitted" && "flex-row",
)}
>
<div className="text-sm text-primary/40">
<div className="flex flex-row items-center gap-1">
{t("explore.plus.submitToPlus.label", {
ns: "components/dialog",
})}
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
</div>
</PopoverTrigger>
<PopoverContent
container={popoverContainerRef.current}
className="w-80 text-xs"
>
{t("explore.plus.submitToPlus.desc", {
{search.data.type === "object" &&
!search.plus_id &&
config?.plus?.enabled && (
<div
className={cn(
"my-2 flex w-full flex-col justify-between gap-1.5",
state == "submitted" && "flex-row",
)}
>
<div className="text-sm text-primary/40">
<div className="flex flex-row items-center gap-1">
{t("explore.plus.submitToPlus.label", {
ns: "components/dialog",
})}
</PopoverContent>
</Popover>
</div>
</div>
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
</div>
</PopoverTrigger>
<PopoverContent
container={popoverContainerRef.current}
className="w-80 text-xs"
>
{t("explore.plus.submitToPlus.desc", {
ns: "components/dialog",
})}
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex flex-row items-center justify-between gap-2 text-sm">
{state == "reviewing" && (
<>
<div>
{i18n.language === "en" ? (
// English with a/an logic plus label
<>
{/^[aeiou]/i.test(search?.label || "") ? (
<Trans
ns="components/dialog"
values={{ label: search?.label }}
>
explore.plus.review.question.ask_an
</Trans>
<div className="flex flex-row items-center justify-between gap-2 text-sm">
{state == "reviewing" && (
<>
<div>
{i18n.language === "en" ? (
// English with a/an logic plus label
<>
{/^[aeiou]/i.test(search?.label || "") ? (
<Trans
ns="components/dialog"
values={{ label: search?.label }}
>
explore.plus.review.question.ask_an
</Trans>
) : (
<Trans
ns="components/dialog"
values={{ label: search?.label }}
>
explore.plus.review.question.ask_a
</Trans>
)}
</>
) : (
// For other languages
<Trans
ns="components/dialog"
values={{ label: search?.label }}
values={{
untranslatedLabel: search?.label,
translatedLabel: getTranslatedLabel(search?.label),
}}
>
explore.plus.review.question.ask_a
explore.plus.review.question.ask_full
</Trans>
)}
</>
) : (
// For other languages
<Trans
ns="components/dialog"
values={{
untranslatedLabel: search?.label,
translatedLabel: getTranslatedLabel(search?.label),
}}
>
explore.plus.review.question.ask_full
</Trans>
)}
</div>
<div className="flex max-w-xl flex-row gap-2">
<Button
className="flex-1 bg-success"
aria-label={t("button.yes", { ns: "common" })}
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
}}
>
{t("button.yes", { ns: "common" })}
</Button>
<Button
className="flex-1 text-white"
aria-label={t("button.no", { ns: "common" })}
variant="destructive"
onClick={() => {
setState("uploading");
onSubmitToPlus(true);
}}
>
{t("button.no", { ns: "common" })}
</Button>
</div>
</>
)}
{state == "uploading" && <ActivityIndicator />}
{state == "submitted" && (
<div className="flex flex-row items-center justify-center gap-2">
<FaCheckCircle className="size-4 text-success" />
{t("explore.plus.review.state.submitted")}
</div>
<div className="flex max-w-xl flex-row gap-2">
<Button
className="flex-1 bg-success"
aria-label={t("button.yes", { ns: "common" })}
onClick={() => {
setState("uploading");
onSubmitToPlus(false);
}}
>
{t("button.yes", { ns: "common" })}
</Button>
<Button
className="flex-1 text-white"
aria-label={t("button.no", { ns: "common" })}
variant="destructive"
onClick={() => {
setState("uploading");
onSubmitToPlus(true);
}}
>
{t("button.no", { ns: "common" })}
</Button>
</div>
</>
)}
{state == "uploading" && <ActivityIndicator />}
{state == "submitted" && (
<div className="flex flex-row items-center justify-center gap-2">
<FaCheckCircle className="size-4 text-success" />
{t("explore.plus.review.state.submitted")}
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
<div className="flex flex-col gap-1.5">
{config?.cameras[search.camera].objects.genai.enabled &&
!search.end_time &&
@@ -457,7 +457,7 @@ export function TrackingDetails({
>
{config?.cameras[event.camera]?.onvif.autotracking
.enabled_in_config && (
<div className="mb-2 text-sm text-danger">
<div className="mb-2 ml-3 text-sm text-danger">
{t("trackingDetails.autoTrackingTips")}
</div>
)}
@@ -20,7 +20,9 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import { isMobile } from "react-device-detect";
import { useRef } from "react";
type PlatformAwareDialogProps = {
trigger: JSX.Element;
@@ -79,6 +81,8 @@ export function PlatformAwareSheet({
open,
onOpenChange,
}: PlatformAwareSheetProps) {
const scrollerRef = useRef<HTMLDivElement>(null);
if (isMobile) {
return (
<MobilePage open={open} onOpenChange={onOpenChange}>
@@ -86,14 +90,22 @@ export function PlatformAwareSheet({
{trigger}
</MobilePageTrigger>
<MobilePagePortal>
<MobilePageContent className="h-full overflow-hidden">
<MobilePageContent
className="flex h-full flex-col"
scrollerRef={scrollerRef}
>
<MobilePageHeader
className="mx-2"
onClose={() => onOpenChange(false)}
>
<MobilePageTitle>{title}</MobilePageTitle>
</MobilePageHeader>
<div className={contentClassName}>{content}</div>
<div
ref={scrollerRef}
className={cn("flex-1 overflow-y-auto", contentClassName)}
>
{content}
</div>
</MobilePageContent>
</MobilePagePortal>
</MobilePage>
@@ -98,7 +98,11 @@ export default function RestartDialog({
open={restartingSheetOpen}
onOpenChange={() => setRestartingSheetOpen(false)}
>
<SheetContent side="top" onInteractOutside={(e) => e.preventDefault()}>
<SheetContent
side="top"
onInteractOutside={(e) => e.preventDefault()}
className="[&>button:first-of-type]:hidden"
>
<div className="flex flex-col items-center">
<ActivityIndicator />
<SheetHeader className="mt-5 text-center">
@@ -230,6 +230,7 @@ export default function SearchFilterDialog({
<PlatformAwareSheet
trigger={trigger}
title={t("more")}
titleClassName="mb-5 -mt-3"
content={content}
contentClassName={cn(
"w-auto lg:min-w-[275px] scrollbar-container h-full overflow-auto px-4",
+2 -2
View File
@@ -192,7 +192,7 @@ export default function DetailStream({
<div className="relative flex h-full flex-col">
<div
ref={scrollRef}
className="scrollbar-container flex-1 overflow-y-auto pb-14"
className="scrollbar-container flex-1 overflow-y-auto overflow-x-hidden pb-14"
>
<div className="space-y-4 py-2">
{reviewItems?.length === 0 ? (
@@ -811,7 +811,7 @@ function ObjectTimeline({
if (!timeline || timeline.length === 0) {
return (
<div className="py-2 text-sm text-muted-foreground">
<div className="ml-8 text-sm text-muted-foreground">
{t("detail.noObjectDetailData")}
</div>
);
+8 -2
View File
@@ -55,20 +55,24 @@ export default function EventMenu({
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuItem onSelect={handleObjectSelect}>
<DropdownMenuItem
className="cursor-pointer"
onSelect={handleObjectSelect}
>
{isSelected
? t("itemMenu.hideObjectDetails.label")
: t("itemMenu.showObjectDetails.label")}
</DropdownMenuItem>
<DropdownMenuSeparator className="my-0.5" />
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => {
navigate(`/explore?event_id=${event.id}`);
}}
>
{t("details.item.button.viewInExplore")}
</DropdownMenuItem>
<DropdownMenuItem asChild>
<DropdownMenuItem className="cursor-pointer" asChild>
<a
download
href={
@@ -86,6 +90,7 @@ export default function EventMenu({
event.data.type == "object" &&
config?.plus?.enabled && (
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => {
setIsOpen(false);
onOpenUpload?.(event);
@@ -97,6 +102,7 @@ export default function EventMenu({
{event.has_snapshot && config?.semantic_search?.enabled && (
<DropdownMenuItem
className="cursor-pointer"
onSelect={() => {
if (onOpenSimilarity) onOpenSimilarity(event);
else
@@ -118,6 +118,11 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
const [trainFilter, setTrainFilter] = useApiFilter<TrainFilter>();
const refreshAll = useCallback(() => {
refreshTrain();
refreshDataset();
}, [refreshTrain, refreshDataset]);
// image multiselect
const [selectedImages, setSelectedImages] = useState<string[]>([]);
@@ -183,11 +188,12 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
);
const onDelete = useCallback(
(ids: string[], isName: boolean = false) => {
(ids: string[], isName: boolean = false, category?: string) => {
const targetCategory = category || pageToggle;
const api =
pageToggle == "train"
targetCategory == "train"
? `/classification/${model.name}/train/delete`
: `/classification/${model.name}/dataset/${pageToggle}/delete`;
: `/classification/${model.name}/dataset/${targetCategory}/delete`;
axios
.post(api, { ids })
@@ -408,7 +414,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
trainImages={trainImages || []}
trainFilter={trainFilter}
selectedImages={selectedImages}
onRefresh={refreshTrain}
onRefresh={refreshAll}
onClickImages={onClickImages}
onDelete={onDelete}
/>
@@ -432,7 +438,7 @@ type LibrarySelectorProps = {
dataset: { [id: string]: string[] };
trainImages: string[];
setPageToggle: (toggle: string) => void;
onDelete: (ids: string[], isName: boolean) => void;
onDelete: (ids: string[], isName: boolean, category?: string) => void;
onRename: (old_name: string, new_name: string) => void;
};
function LibrarySelector({
@@ -448,7 +454,7 @@ function LibrarySelector({
// data
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
const [renameClass, setRenameFace] = useState<string | null>(null);
const [renameClass, setRenameClass] = useState<string | null>(null);
const pageTitle = useMemo(() => {
if (pageToggle != "train") {
return pageToggle;
@@ -463,12 +469,12 @@ function LibrarySelector({
// interaction
const handleDeleteFace = useCallback(
const handleDeleteCategory = useCallback(
(name: string) => {
// Get all image IDs for this face
// Get all image IDs for this category
const imageIds = dataset?.[name] || [];
onDelete(imageIds, true);
onDelete(imageIds, true, name);
setPageToggle("train");
},
[dataset, onDelete, setPageToggle],
@@ -476,7 +482,7 @@ function LibrarySelector({
const handleSetOpen = useCallback(
(open: boolean) => {
setRenameFace(open ? renameClass : null);
setRenameClass(open ? renameClass : null);
},
[renameClass],
);
@@ -503,7 +509,7 @@ function LibrarySelector({
className="text-white"
onClick={() => {
if (confirmDelete) {
handleDeleteFace(confirmDelete);
handleDeleteCategory(confirmDelete);
setConfirmDelete(null);
}
}}
@@ -521,7 +527,7 @@ function LibrarySelector({
description={t("renameCategory.desc", { name: renameClass })}
onSave={(newName) => {
onRename(renameClass!, newName);
setRenameFace(null);
setRenameClass(null);
}}
defaultValue={renameClass || ""}
regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u}
@@ -588,7 +594,7 @@ function LibrarySelector({
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
setRenameFace(id);
setRenameClass(id);
}}
>
<LuPencil className="size-4 text-primary" />
-5
View File
@@ -236,10 +236,6 @@ function ExploreThumbnailImage({
onSelectSearch(event, false, "tracking_details");
};
const handleShowSnapshot = () => {
onSelectSearch(event, false, "snapshot");
};
const handleAddTrigger = () => {
navigate(
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
@@ -252,7 +248,6 @@ function ExploreThumbnailImage({
findSimilar={handleFindSimilar}
refreshResults={mutate}
showTrackingDetails={handleShowTrackingDetails}
showSnapshot={handleShowSnapshot}
addTrigger={handleAddTrigger}
isContextMenu={true}
>
+1 -1
View File
@@ -985,7 +985,7 @@ function Timeline({
),
)}
>
{isMobile && (
{isMobile && timelineType == "timeline" && (
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
)}
-3
View File
@@ -688,9 +688,6 @@ export default function SearchView({
showTrackingDetails={() =>
onSelectSearch(value, false, "tracking_details")
}
showSnapshot={() =>
onSelectSearch(value, false, "snapshot")
}
addTrigger={() => {
if (
config?.semantic_search.enabled &&
+2 -1
View File
@@ -403,7 +403,8 @@ export default function TriggerView({
setShowCreate(true);
setSelectedTrigger({
enabled: true,
name: "",
name: eventId,
friendly_name: "",
type: "thumbnail",
data: eventId,
threshold: 0.5,