mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
Miscellaneous fixes (#20833)
* remove frigate+ icon from explore grid footer * add margin * pointer cursor on event menu items in detail stream * don't show submit to plus for non-objects and if plus is disabled * tweak spacing in annotation settings popover * Fix deletion of classification images and library * Ensure after creating a class that things are correct * Fix dialog getting stuck * Only show the genai summary popup on mobile when timeline is open * fix audio transcription embedding * spacing * hide x icon on restart sheet to prevent closure issues * prevent x overflow in detail stream on mobile safari * ensure name is valid for search effect trigger * add trigger to detail actions menu * move find similar to actions menu * 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 * Skip azure execution provider * 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 * fix title classes on desktop --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
parent
a15399fed5
commit
530b69b877
@ -662,8 +662,11 @@ def delete_classification_dataset_images(
|
|||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
os.unlink(file_path)
|
os.unlink(file_path)
|
||||||
|
|
||||||
|
if os.path.exists(folder) and not os.listdir(folder):
|
||||||
|
os.rmdir(folder)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
content=({"success": True, "message": "Successfully deleted images."}),
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -723,7 +726,7 @@ def categorize_classification_image(request: Request, name: str, body: dict = No
|
|||||||
os.unlink(training_file)
|
os.unlink(training_file)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
content=({"success": True, "message": "Successfully categorized image."}),
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -761,7 +764,7 @@ def delete_classification_train_images(request: Request, name: str, body: dict =
|
|||||||
os.unlink(file_path)
|
os.unlink(file_path)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=({"success": True, "message": "Successfully deleted faces."}),
|
content=({"success": True, "message": "Successfully deleted images."}),
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ from typing import Optional
|
|||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
from frigate.comms.embeddings_updater import EmbeddingsRequestEnum
|
|
||||||
from frigate.comms.inter_process import InterProcessRequestor
|
from frigate.comms.inter_process import InterProcessRequestor
|
||||||
from frigate.config import FrigateConfig
|
from frigate.config import FrigateConfig
|
||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
@ -32,11 +31,13 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
self,
|
self,
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
requestor: InterProcessRequestor,
|
requestor: InterProcessRequestor,
|
||||||
|
embeddings,
|
||||||
metrics: DataProcessorMetrics,
|
metrics: DataProcessorMetrics,
|
||||||
):
|
):
|
||||||
super().__init__(config, metrics, None)
|
super().__init__(config, metrics, None)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.requestor = requestor
|
self.requestor = requestor
|
||||||
|
self.embeddings = embeddings
|
||||||
self.recognizer = None
|
self.recognizer = None
|
||||||
self.transcription_lock = threading.Lock()
|
self.transcription_lock = threading.Lock()
|
||||||
self.transcription_thread = None
|
self.transcription_thread = None
|
||||||
@ -128,10 +129,7 @@ class AudioTranscriptionPostProcessor(PostProcessorApi):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Embed the description
|
# Embed the description
|
||||||
self.requestor.send_data(
|
self.embeddings.embed_description(event_id, transcription)
|
||||||
EmbeddingsRequestEnum.embed_description.value,
|
|
||||||
{"id": event_id, "description": transcription},
|
|
||||||
)
|
|
||||||
|
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
logger.debug("No recording found for audio transcription post-processing")
|
logger.debug("No recording found for audio transcription post-processing")
|
||||||
|
|||||||
@ -226,7 +226,9 @@ class EmbeddingMaintainer(threading.Thread):
|
|||||||
for c in self.config.cameras.values()
|
for c in self.config.cameras.values()
|
||||||
):
|
):
|
||||||
self.post_processors.append(
|
self.post_processors.append(
|
||||||
AudioTranscriptionPostProcessor(self.config, self.requestor, metrics)
|
AudioTranscriptionPostProcessor(
|
||||||
|
self.config, self.requestor, self.embeddings, metrics
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
semantic_trigger_processor: SemanticTriggerProcessor | None = None
|
semantic_trigger_processor: SemanticTriggerProcessor | None = None
|
||||||
|
|||||||
@ -369,6 +369,10 @@ def get_ort_providers(
|
|||||||
"enable_cpu_mem_arena": False,
|
"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:
|
else:
|
||||||
providers.append(provider)
|
providers.append(provider)
|
||||||
options.append({})
|
options.append({})
|
||||||
|
|||||||
@ -14,7 +14,6 @@ type SearchThumbnailProps = {
|
|||||||
findSimilar: () => void;
|
findSimilar: () => void;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
showTrackingDetails: () => void;
|
showTrackingDetails: () => void;
|
||||||
showSnapshot: () => void;
|
|
||||||
addTrigger: () => void;
|
addTrigger: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,7 +23,6 @@ export default function SearchThumbnailFooter({
|
|||||||
findSimilar,
|
findSimilar,
|
||||||
refreshResults,
|
refreshResults,
|
||||||
showTrackingDetails,
|
showTrackingDetails,
|
||||||
showSnapshot,
|
|
||||||
addTrigger,
|
addTrigger,
|
||||||
}: SearchThumbnailProps) {
|
}: SearchThumbnailProps) {
|
||||||
const { t } = useTranslation(["views/search"]);
|
const { t } = useTranslation(["views/search"]);
|
||||||
@ -62,7 +60,6 @@ export default function SearchThumbnailFooter({
|
|||||||
findSimilar={findSimilar}
|
findSimilar={findSimilar}
|
||||||
refreshResults={refreshResults}
|
refreshResults={refreshResults}
|
||||||
showTrackingDetails={showTrackingDetails}
|
showTrackingDetails={showTrackingDetails}
|
||||||
showSnapshot={showSnapshot}
|
|
||||||
addTrigger={addTrigger}
|
addTrigger={addTrigger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,10 +6,7 @@ import { toast } from "sonner";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { LuCamera, LuDownload, LuTrash2 } from "react-icons/lu";
|
import { LuCamera, LuDownload, LuTrash2 } from "react-icons/lu";
|
||||||
import { FiMoreVertical } from "react-icons/fi";
|
import { FiMoreVertical } from "react-icons/fi";
|
||||||
import { FaArrowsRotate } from "react-icons/fa6";
|
|
||||||
import { MdImageSearch } from "react-icons/md";
|
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 { buttonVariants } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@ -33,23 +30,18 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { BsFillLightningFill } from "react-icons/bs";
|
import { BsFillLightningFill } from "react-icons/bs";
|
||||||
import BlurredIconButton from "../button/BlurredIconButton";
|
import BlurredIconButton from "../button/BlurredIconButton";
|
||||||
|
import { PiPath } from "react-icons/pi";
|
||||||
|
|
||||||
type SearchResultActionsProps = {
|
type SearchResultActionsProps = {
|
||||||
searchResult: SearchResult;
|
searchResult: SearchResult;
|
||||||
findSimilar: () => void;
|
findSimilar: () => void;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
showTrackingDetails: () => void;
|
showTrackingDetails: () => void;
|
||||||
showSnapshot: () => void;
|
|
||||||
addTrigger: () => void;
|
addTrigger: () => void;
|
||||||
isContextMenu?: boolean;
|
isContextMenu?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@ -60,7 +52,6 @@ export default function SearchResultActions({
|
|||||||
findSimilar,
|
findSimilar,
|
||||||
refreshResults,
|
refreshResults,
|
||||||
showTrackingDetails,
|
showTrackingDetails,
|
||||||
showSnapshot,
|
|
||||||
addTrigger,
|
addTrigger,
|
||||||
isContextMenu = false,
|
isContextMenu = false,
|
||||||
children,
|
children,
|
||||||
@ -129,7 +120,7 @@ export default function SearchResultActions({
|
|||||||
aria-label={t("itemMenu.viewTrackingDetails.aria")}
|
aria-label={t("itemMenu.viewTrackingDetails.aria")}
|
||||||
onClick={showTrackingDetails}
|
onClick={showTrackingDetails}
|
||||||
>
|
>
|
||||||
<FaArrowsRotate className="mr-2 size-4" />
|
<PiPath className="mr-2 size-4" />
|
||||||
<span>{t("itemMenu.viewTrackingDetails.label")}</span>
|
<span>{t("itemMenu.viewTrackingDetails.label")}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
@ -152,18 +143,14 @@ export default function SearchResultActions({
|
|||||||
<span>{t("itemMenu.addTrigger.label")}</span>
|
<span>{t("itemMenu.addTrigger.label")}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{isMobileOnly &&
|
{config?.semantic_search?.enabled &&
|
||||||
config?.plus?.enabled &&
|
searchResult.data.type == "object" && (
|
||||||
searchResult.has_snapshot &&
|
|
||||||
searchResult.end_time &&
|
|
||||||
searchResult.data.type == "object" &&
|
|
||||||
!searchResult.plus_id && (
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
aria-label={t("itemMenu.submitToPlus.aria")}
|
aria-label={t("itemMenu.findSimilar.aria")}
|
||||||
onClick={showSnapshot}
|
onClick={findSimilar}
|
||||||
>
|
>
|
||||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
<MdImageSearch className="mr-2 size-4" />
|
||||||
<span>{t("itemMenu.submitToPlus.label")}</span>
|
<span>{t("itemMenu.findSimilar.label")}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -211,44 +198,6 @@ export default function SearchResultActions({
|
|||||||
</ContextMenu>
|
</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>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
@ -121,17 +122,20 @@ export function MobilePagePortal({
|
|||||||
type MobilePageContentProps = {
|
type MobilePageContentProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
scrollerRef?: React.RefObject<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MobilePageContent({
|
export function MobilePageContent({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
scrollerRef,
|
||||||
}: MobilePageContentProps) {
|
}: MobilePageContentProps) {
|
||||||
const context = useContext(MobilePageContext);
|
const context = useContext(MobilePageContext);
|
||||||
if (!context)
|
if (!context)
|
||||||
throw new Error("MobilePageContent must be used within MobilePage");
|
throw new Error("MobilePageContent must be used within MobilePage");
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(context.open);
|
const [isVisible, setIsVisible] = useState(context.open);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (context.open) {
|
if (context.open) {
|
||||||
@ -140,15 +144,27 @@ export function MobilePageContent({
|
|||||||
}, [context.open]);
|
}, [context.open]);
|
||||||
|
|
||||||
const handleAnimationComplete = () => {
|
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);
|
setIsVisible(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (context.open && scrollerRef?.current) {
|
||||||
|
scrollerRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, [context.open, scrollerRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isVisible && (
|
{isVisible && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 mb-12 bg-background",
|
"fixed inset-0 z-50 mb-12 bg-background",
|
||||||
isPWA && "mb-16",
|
isPWA && "mb-16",
|
||||||
|
|||||||
@ -97,14 +97,12 @@ export default function ClassificationSelectionDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className ?? "flex"}>
|
<div className={className ?? "flex"}>
|
||||||
{newClass && (
|
|
||||||
<TextEntryDialog
|
<TextEntryDialog
|
||||||
open={true}
|
open={newClass}
|
||||||
setOpen={setNewClass}
|
setOpen={setNewClass}
|
||||||
title={t("createCategory.new")}
|
title={t("createCategory.new")}
|
||||||
onSave={(newCat) => onCategorizeImage(newCat)}
|
onSave={(newCat) => onCategorizeImage(newCat)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<Selector>
|
<Selector>
|
||||||
|
|||||||
@ -141,12 +141,13 @@ export function AnnotationSettingsPane({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="flex flex-1 flex-col space-y-6"
|
className="flex flex-1 flex-col space-y-3"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="annotationOffset"
|
name="annotationOffset"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
<>
|
||||||
<FormItem className="flex flex-row items-start justify-between space-x-2">
|
<FormItem className="flex flex-row items-start justify-between space-x-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@ -157,20 +158,6 @@ export function AnnotationSettingsPane({
|
|||||||
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
trackingDetails.annotationSettings.offset.millisecondsToOffset
|
||||||
</Trans>
|
</Trans>
|
||||||
<FormMessage />
|
<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>
|
|
||||||
</div>
|
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
@ -185,6 +172,21 @@ export function AnnotationSettingsPane({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,23 @@ export default function DetailActionsMenu({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</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>
|
</DropdownMenuContent>
|
||||||
</DropdownMenuPortal>
|
</DropdownMenuPortal>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -1242,6 +1242,9 @@ function ObjectDetailsTab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{search.data.type === "object" &&
|
||||||
|
!search.plus_id &&
|
||||||
|
config?.plus?.enabled && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"my-2 flex w-full flex-col justify-between gap-1.5",
|
"my-2 flex w-full flex-col justify-between gap-1.5",
|
||||||
@ -1342,6 +1345,7 @@ function ObjectDetailsTab({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{config?.cameras[search.camera].objects.genai.enabled &&
|
{config?.cameras[search.camera].objects.genai.enabled &&
|
||||||
!search.end_time &&
|
!search.end_time &&
|
||||||
|
|||||||
@ -457,7 +457,7 @@ export function TrackingDetails({
|
|||||||
>
|
>
|
||||||
{config?.cameras[event.camera]?.onvif.autotracking
|
{config?.cameras[event.camera]?.onvif.autotracking
|
||||||
.enabled_in_config && (
|
.enabled_in_config && (
|
||||||
<div className="mb-2 text-sm text-danger">
|
<div className="mb-2 ml-3 text-sm text-danger">
|
||||||
{t("trackingDetails.autoTrackingTips")}
|
{t("trackingDetails.autoTrackingTips")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -20,7 +20,9 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
type PlatformAwareDialogProps = {
|
type PlatformAwareDialogProps = {
|
||||||
trigger: JSX.Element;
|
trigger: JSX.Element;
|
||||||
@ -79,6 +81,8 @@ export function PlatformAwareSheet({
|
|||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: PlatformAwareSheetProps) {
|
}: PlatformAwareSheetProps) {
|
||||||
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<MobilePage open={open} onOpenChange={onOpenChange}>
|
<MobilePage open={open} onOpenChange={onOpenChange}>
|
||||||
@ -86,14 +90,22 @@ export function PlatformAwareSheet({
|
|||||||
{trigger}
|
{trigger}
|
||||||
</MobilePageTrigger>
|
</MobilePageTrigger>
|
||||||
<MobilePagePortal>
|
<MobilePagePortal>
|
||||||
<MobilePageContent className="h-full overflow-hidden">
|
<MobilePageContent
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
scrollerRef={scrollerRef}
|
||||||
|
>
|
||||||
<MobilePageHeader
|
<MobilePageHeader
|
||||||
className="mx-2"
|
className="mx-2"
|
||||||
onClose={() => onOpenChange(false)}
|
onClose={() => onOpenChange(false)}
|
||||||
>
|
>
|
||||||
<MobilePageTitle>{title}</MobilePageTitle>
|
<MobilePageTitle>{title}</MobilePageTitle>
|
||||||
</MobilePageHeader>
|
</MobilePageHeader>
|
||||||
<div className={contentClassName}>{content}</div>
|
<div
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={cn("flex-1 overflow-y-auto", contentClassName)}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
</MobilePageContent>
|
</MobilePageContent>
|
||||||
</MobilePagePortal>
|
</MobilePagePortal>
|
||||||
</MobilePage>
|
</MobilePage>
|
||||||
|
|||||||
@ -98,7 +98,11 @@ export default function RestartDialog({
|
|||||||
open={restartingSheetOpen}
|
open={restartingSheetOpen}
|
||||||
onOpenChange={() => setRestartingSheetOpen(false)}
|
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">
|
<div className="flex flex-col items-center">
|
||||||
<ActivityIndicator />
|
<ActivityIndicator />
|
||||||
<SheetHeader className="mt-5 text-center">
|
<SheetHeader className="mt-5 text-center">
|
||||||
|
|||||||
@ -230,6 +230,7 @@ export default function SearchFilterDialog({
|
|||||||
<PlatformAwareSheet
|
<PlatformAwareSheet
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
title={t("more")}
|
title={t("more")}
|
||||||
|
titleClassName="mb-5 -mt-3"
|
||||||
content={content}
|
content={content}
|
||||||
contentClassName={cn(
|
contentClassName={cn(
|
||||||
"w-auto lg:min-w-[275px] scrollbar-container h-full overflow-auto px-4",
|
"w-auto lg:min-w-[275px] scrollbar-container h-full overflow-auto px-4",
|
||||||
|
|||||||
@ -192,7 +192,7 @@ export default function DetailStream({
|
|||||||
<div className="relative flex h-full flex-col">
|
<div className="relative flex h-full flex-col">
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
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">
|
<div className="space-y-4 py-2">
|
||||||
{reviewItems?.length === 0 ? (
|
{reviewItems?.length === 0 ? (
|
||||||
@ -811,7 +811,7 @@ function ObjectTimeline({
|
|||||||
|
|
||||||
if (!timeline || timeline.length === 0) {
|
if (!timeline || timeline.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="py-2 text-sm text-muted-foreground">
|
<div className="ml-8 text-sm text-muted-foreground">
|
||||||
{t("detail.noObjectDetailData")}
|
{t("detail.noObjectDetailData")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -55,20 +55,24 @@ export default function EventMenu({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem onSelect={handleObjectSelect}>
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={handleObjectSelect}
|
||||||
|
>
|
||||||
{isSelected
|
{isSelected
|
||||||
? t("itemMenu.hideObjectDetails.label")
|
? t("itemMenu.hideObjectDetails.label")
|
||||||
: t("itemMenu.showObjectDetails.label")}
|
: t("itemMenu.showObjectDetails.label")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator className="my-0.5" />
|
<DropdownMenuSeparator className="my-0.5" />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate(`/explore?event_id=${event.id}`);
|
navigate(`/explore?event_id=${event.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("details.item.button.viewInExplore")}
|
{t("details.item.button.viewInExplore")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||||
<a
|
<a
|
||||||
download
|
download
|
||||||
href={
|
href={
|
||||||
@ -86,6 +90,7 @@ export default function EventMenu({
|
|||||||
event.data.type == "object" &&
|
event.data.type == "object" &&
|
||||||
config?.plus?.enabled && (
|
config?.plus?.enabled && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
onOpenUpload?.(event);
|
onOpenUpload?.(event);
|
||||||
@ -97,6 +102,7 @@ export default function EventMenu({
|
|||||||
|
|
||||||
{event.has_snapshot && config?.semantic_search?.enabled && (
|
{event.has_snapshot && config?.semantic_search?.enabled && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
className="cursor-pointer"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (onOpenSimilarity) onOpenSimilarity(event);
|
if (onOpenSimilarity) onOpenSimilarity(event);
|
||||||
else
|
else
|
||||||
|
|||||||
@ -118,6 +118,11 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
|
|
||||||
const [trainFilter, setTrainFilter] = useApiFilter<TrainFilter>();
|
const [trainFilter, setTrainFilter] = useApiFilter<TrainFilter>();
|
||||||
|
|
||||||
|
const refreshAll = useCallback(() => {
|
||||||
|
refreshTrain();
|
||||||
|
refreshDataset();
|
||||||
|
}, [refreshTrain, refreshDataset]);
|
||||||
|
|
||||||
// image multiselect
|
// image multiselect
|
||||||
|
|
||||||
const [selectedImages, setSelectedImages] = useState<string[]>([]);
|
const [selectedImages, setSelectedImages] = useState<string[]>([]);
|
||||||
@ -183,11 +188,12 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(ids: string[], isName: boolean = false) => {
|
(ids: string[], isName: boolean = false, category?: string) => {
|
||||||
|
const targetCategory = category || pageToggle;
|
||||||
const api =
|
const api =
|
||||||
pageToggle == "train"
|
targetCategory == "train"
|
||||||
? `/classification/${model.name}/train/delete`
|
? `/classification/${model.name}/train/delete`
|
||||||
: `/classification/${model.name}/dataset/${pageToggle}/delete`;
|
: `/classification/${model.name}/dataset/${targetCategory}/delete`;
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(api, { ids })
|
.post(api, { ids })
|
||||||
@ -408,7 +414,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
|
|||||||
trainImages={trainImages || []}
|
trainImages={trainImages || []}
|
||||||
trainFilter={trainFilter}
|
trainFilter={trainFilter}
|
||||||
selectedImages={selectedImages}
|
selectedImages={selectedImages}
|
||||||
onRefresh={refreshTrain}
|
onRefresh={refreshAll}
|
||||||
onClickImages={onClickImages}
|
onClickImages={onClickImages}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
@ -432,7 +438,7 @@ type LibrarySelectorProps = {
|
|||||||
dataset: { [id: string]: string[] };
|
dataset: { [id: string]: string[] };
|
||||||
trainImages: string[];
|
trainImages: string[];
|
||||||
setPageToggle: (toggle: string) => void;
|
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;
|
onRename: (old_name: string, new_name: string) => void;
|
||||||
};
|
};
|
||||||
function LibrarySelector({
|
function LibrarySelector({
|
||||||
@ -448,7 +454,7 @@ function LibrarySelector({
|
|||||||
// data
|
// data
|
||||||
|
|
||||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||||
const [renameClass, setRenameFace] = useState<string | null>(null);
|
const [renameClass, setRenameClass] = useState<string | null>(null);
|
||||||
const pageTitle = useMemo(() => {
|
const pageTitle = useMemo(() => {
|
||||||
if (pageToggle != "train") {
|
if (pageToggle != "train") {
|
||||||
return pageToggle;
|
return pageToggle;
|
||||||
@ -463,12 +469,12 @@ function LibrarySelector({
|
|||||||
|
|
||||||
// interaction
|
// interaction
|
||||||
|
|
||||||
const handleDeleteFace = useCallback(
|
const handleDeleteCategory = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
// Get all image IDs for this face
|
// Get all image IDs for this category
|
||||||
const imageIds = dataset?.[name] || [];
|
const imageIds = dataset?.[name] || [];
|
||||||
|
|
||||||
onDelete(imageIds, true);
|
onDelete(imageIds, true, name);
|
||||||
setPageToggle("train");
|
setPageToggle("train");
|
||||||
},
|
},
|
||||||
[dataset, onDelete, setPageToggle],
|
[dataset, onDelete, setPageToggle],
|
||||||
@ -476,7 +482,7 @@ function LibrarySelector({
|
|||||||
|
|
||||||
const handleSetOpen = useCallback(
|
const handleSetOpen = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
setRenameFace(open ? renameClass : null);
|
setRenameClass(open ? renameClass : null);
|
||||||
},
|
},
|
||||||
[renameClass],
|
[renameClass],
|
||||||
);
|
);
|
||||||
@ -503,7 +509,7 @@ function LibrarySelector({
|
|||||||
className="text-white"
|
className="text-white"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirmDelete) {
|
if (confirmDelete) {
|
||||||
handleDeleteFace(confirmDelete);
|
handleDeleteCategory(confirmDelete);
|
||||||
setConfirmDelete(null);
|
setConfirmDelete(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@ -521,7 +527,7 @@ function LibrarySelector({
|
|||||||
description={t("renameCategory.desc", { name: renameClass })}
|
description={t("renameCategory.desc", { name: renameClass })}
|
||||||
onSave={(newName) => {
|
onSave={(newName) => {
|
||||||
onRename(renameClass!, newName);
|
onRename(renameClass!, newName);
|
||||||
setRenameFace(null);
|
setRenameClass(null);
|
||||||
}}
|
}}
|
||||||
defaultValue={renameClass || ""}
|
defaultValue={renameClass || ""}
|
||||||
regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u}
|
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"
|
className="size-7 lg:opacity-0 lg:transition-opacity lg:group-hover:opacity-100"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setRenameFace(id);
|
setRenameClass(id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LuPencil className="size-4 text-primary" />
|
<LuPencil className="size-4 text-primary" />
|
||||||
|
|||||||
@ -236,10 +236,6 @@ function ExploreThumbnailImage({
|
|||||||
onSelectSearch(event, false, "tracking_details");
|
onSelectSearch(event, false, "tracking_details");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShowSnapshot = () => {
|
|
||||||
onSelectSearch(event, false, "snapshot");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddTrigger = () => {
|
const handleAddTrigger = () => {
|
||||||
navigate(
|
navigate(
|
||||||
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
|
`/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`,
|
||||||
@ -252,7 +248,6 @@ function ExploreThumbnailImage({
|
|||||||
findSimilar={handleFindSimilar}
|
findSimilar={handleFindSimilar}
|
||||||
refreshResults={mutate}
|
refreshResults={mutate}
|
||||||
showTrackingDetails={handleShowTrackingDetails}
|
showTrackingDetails={handleShowTrackingDetails}
|
||||||
showSnapshot={handleShowSnapshot}
|
|
||||||
addTrigger={handleAddTrigger}
|
addTrigger={handleAddTrigger}
|
||||||
isContextMenu={true}
|
isContextMenu={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -985,7 +985,7 @@ function Timeline({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isMobile && (
|
{isMobile && timelineType == "timeline" && (
|
||||||
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
|
<GenAISummaryDialog review={activeReviewItem} onOpen={onAnalysisOpen} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -688,9 +688,6 @@ export default function SearchView({
|
|||||||
showTrackingDetails={() =>
|
showTrackingDetails={() =>
|
||||||
onSelectSearch(value, false, "tracking_details")
|
onSelectSearch(value, false, "tracking_details")
|
||||||
}
|
}
|
||||||
showSnapshot={() =>
|
|
||||||
onSelectSearch(value, false, "snapshot")
|
|
||||||
}
|
|
||||||
addTrigger={() => {
|
addTrigger={() => {
|
||||||
if (
|
if (
|
||||||
config?.semantic_search.enabled &&
|
config?.semantic_search.enabled &&
|
||||||
|
|||||||
@ -403,7 +403,8 @@ export default function TriggerView({
|
|||||||
setShowCreate(true);
|
setShowCreate(true);
|
||||||
setSelectedTrigger({
|
setSelectedTrigger({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: "",
|
name: eventId,
|
||||||
|
friendly_name: "",
|
||||||
type: "thumbnail",
|
type: "thumbnail",
|
||||||
data: eventId,
|
data: eventId,
|
||||||
threshold: 0.5,
|
threshold: 0.5,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user