mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
UI tweaks (#13633)
* Object lifecycle and semantic search UI tweaks * prevent console errors for sheet component
This commit is contained in:
parent
8be139d4d1
commit
f143fceceb
@ -379,7 +379,12 @@ def events_search():
|
||||
n_results=limit,
|
||||
where=where,
|
||||
)
|
||||
thumb_ids = dict(zip(thumb_result["ids"][0], thumb_result["distances"][0]))
|
||||
thumb_ids = dict(
|
||||
zip(
|
||||
thumb_result["ids"][0],
|
||||
context.thumb_stats.normalize(thumb_result["distances"][0]),
|
||||
)
|
||||
)
|
||||
else:
|
||||
thumb_result = context.embeddings.thumbnail.query(
|
||||
query_texts=[query],
|
||||
|
@ -1,7 +1,19 @@
|
||||
import { LogLine } from "@/types/log";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { Sheet, SheetContent } from "../ui/sheet";
|
||||
import { Drawer, DrawerContent } from "../ui/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "../ui/sheet";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "../ui/drawer";
|
||||
import { LogChip } from "../indicators/Chip";
|
||||
import { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -16,6 +28,9 @@ export default function LogInfoDialog({
|
||||
}: LogInfoDialogProps) {
|
||||
const Overlay = isDesktop ? Sheet : Drawer;
|
||||
const Content = isDesktop ? SheetContent : DrawerContent;
|
||||
const Header = isDesktop ? SheetHeader : DrawerHeader;
|
||||
const Title = isDesktop ? SheetTitle : DrawerTitle;
|
||||
const Description = isDesktop ? SheetDescription : DrawerDescription;
|
||||
|
||||
const helpfulLinks = useHelpfulLinks(logLine?.content);
|
||||
|
||||
@ -31,6 +46,10 @@ export default function LogInfoDialog({
|
||||
<Content
|
||||
className={isDesktop ? "" : "max-h-[75dvh] overflow-hidden p-2 pb-4"}
|
||||
>
|
||||
<Header className="sr-only">
|
||||
<Title>Log Details</Title>
|
||||
<Description>Log details</Description>
|
||||
</Header>
|
||||
{logLine && (
|
||||
<div className="flex size-full flex-col gap-5">
|
||||
<div className="flex w-min flex-col gap-1.5">
|
||||
|
@ -44,6 +44,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
|
||||
type ObjectLifecycleProps = {
|
||||
review: ReviewSegment;
|
||||
@ -185,7 +186,6 @@ export default function ObjectLifecycle({
|
||||
if (!mainApi || !thumbnailApi) {
|
||||
return;
|
||||
}
|
||||
thumbnailApi.scrollTo(index);
|
||||
mainApi.scrollTo(index);
|
||||
setCurrent(index);
|
||||
};
|
||||
@ -210,18 +210,10 @@ export default function ObjectLifecycle({
|
||||
thumbnailApi.scrollTo(selected);
|
||||
};
|
||||
|
||||
const handleBottomSelect = () => {
|
||||
const selected = thumbnailApi.selectedScrollSnap();
|
||||
setCurrent(selected);
|
||||
mainApi.scrollTo(selected);
|
||||
};
|
||||
|
||||
mainApi.on("select", handleTopSelect);
|
||||
thumbnailApi.on("select", handleBottomSelect);
|
||||
mainApi.on("select", handleTopSelect).on("reInit", handleTopSelect);
|
||||
|
||||
return () => {
|
||||
mainApi.off("select", handleTopSelect);
|
||||
thumbnailApi.off("select", handleBottomSelect);
|
||||
};
|
||||
// we know that these deps are correct
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -467,15 +459,22 @@ export default function ObjectLifecycle({
|
||||
<Carousel
|
||||
opts={{
|
||||
align: "center",
|
||||
containScroll: "keepSnaps",
|
||||
dragFree: true,
|
||||
}}
|
||||
className="w-full max-w-[72%] md:max-w-[85%]"
|
||||
setApi={setThumbnailApi}
|
||||
>
|
||||
<CarouselContent className="flex flex-row justify-center">
|
||||
<CarouselContent
|
||||
className={cn(
|
||||
"-ml-1 flex select-none flex-row",
|
||||
eventSequence.length > 4 ? "justify-start" : "justify-center",
|
||||
)}
|
||||
>
|
||||
{eventSequence.map((item, index) => (
|
||||
<CarouselItem
|
||||
key={index}
|
||||
className={cn("basis-1/4 cursor-pointer md:basis-[10%]")}
|
||||
className={cn("basis-1/4 cursor-pointer pl-1 md:basis-[10%]")}
|
||||
onClick={() => handleThumbnailClick(index)}
|
||||
>
|
||||
<div className="p-1">
|
||||
@ -486,15 +485,24 @@ export default function ObjectLifecycle({
|
||||
index === current && "bg-selected",
|
||||
)}
|
||||
>
|
||||
<LifecycleIcon
|
||||
className={cn(
|
||||
"size-8",
|
||||
index === current
|
||||
? "bg-selected text-white"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
lifecycleItem={item}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LifecycleIcon
|
||||
className={cn(
|
||||
"size-8",
|
||||
index === current
|
||||
? "bg-selected text-white"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
lifecycleItem={item}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent className="capitalize">
|
||||
{getLifecycleItemDescription(item)}
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { isDesktop, isIOS, isMobile } from "react-device-detect";
|
||||
import { Sheet, SheetContent } from "../../ui/sheet";
|
||||
import { Drawer, DrawerContent } from "../../ui/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "../../ui/sheet";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "../../ui/drawer";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||
@ -66,6 +78,9 @@ export default function ReviewDetailDialog({
|
||||
|
||||
const Overlay = isDesktop ? Sheet : Drawer;
|
||||
const Content = isDesktop ? SheetContent : DrawerContent;
|
||||
const Header = isDesktop ? SheetHeader : DrawerHeader;
|
||||
const Title = isDesktop ? SheetTitle : DrawerTitle;
|
||||
const Description = isDesktop ? SheetDescription : DrawerDescription;
|
||||
|
||||
if (!review) {
|
||||
return;
|
||||
@ -102,6 +117,10 @@ export default function ReviewDetailDialog({
|
||||
: "max-h-[80dvh] overflow-hidden p-2 pb-4",
|
||||
)}
|
||||
>
|
||||
<Header className="sr-only">
|
||||
<Title>Review Item Details</Title>
|
||||
<Description>Review item details</Description>
|
||||
</Header>
|
||||
{pane == "overview" && (
|
||||
<div className="scrollbar-container mt-3 flex size-full flex-col gap-5 overflow-y-auto">
|
||||
<div className="flex w-full flex-row">
|
||||
|
@ -1,6 +1,18 @@
|
||||
import { isDesktop, isIOS } from "react-device-detect";
|
||||
import { Sheet, SheetContent } from "../../ui/sheet";
|
||||
import { Drawer, DrawerContent } from "../../ui/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "../../ui/sheet";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "../../ui/drawer";
|
||||
import { SearchResult } from "@/types/search";
|
||||
import useSWR from "swr";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
@ -71,6 +83,9 @@ export default function SearchDetailDialog({
|
||||
|
||||
const Overlay = isDesktop ? Sheet : Drawer;
|
||||
const Content = isDesktop ? SheetContent : DrawerContent;
|
||||
const Header = isDesktop ? SheetHeader : DrawerHeader;
|
||||
const Title = isDesktop ? SheetTitle : DrawerTitle;
|
||||
const Description = isDesktop ? SheetDescription : DrawerDescription;
|
||||
|
||||
return (
|
||||
<Overlay
|
||||
@ -86,6 +101,10 @@ export default function SearchDetailDialog({
|
||||
isDesktop ? "sm:max-w-xl" : "max-h-[75dvh] overflow-hidden p-2 pb-4"
|
||||
}
|
||||
>
|
||||
<Header className="sr-only">
|
||||
<Title>Tracked Object Details</Title>
|
||||
<Description>Tracked object details</Description>
|
||||
</Header>
|
||||
{search && (
|
||||
<div className="mt-3 flex size-full flex-col gap-5 md:mt-0">
|
||||
<div className="flex w-full flex-row">
|
||||
@ -93,7 +112,7 @@ export default function SearchDetailDialog({
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-sm text-primary/40">Label</div>
|
||||
<div className="flex flex-row items-center gap-2 text-sm capitalize">
|
||||
{getIconForLabel(search.label, "size-4 text-white")}
|
||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||
{search.label}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -287,17 +287,6 @@ function PreviewContent({
|
||||
/>
|
||||
);
|
||||
} else if (isCurrentHour(searchResult.start_time)) {
|
||||
return (
|
||||
/*<InProgressPreview
|
||||
review={review}
|
||||
timeRange={timeRange}
|
||||
setIgnoreClick={setIgnoreClick}
|
||||
isPlayingBack={isPlayingBack}
|
||||
onTimeUpdate={onTimeUpdate}
|
||||
windowVisible={true}
|
||||
setReviewed={() => { }}
|
||||
/>*/
|
||||
<div />
|
||||
);
|
||||
return <div />;
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export default function Search() {
|
||||
setTimeout(() => {
|
||||
setSearchTimeout(undefined);
|
||||
setSearchTerm(search);
|
||||
}, 500),
|
||||
}, 750),
|
||||
);
|
||||
// we only want to update the searchTerm when search changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
@ -12,6 +12,7 @@ export type SearchResult = {
|
||||
thumb_path?: string;
|
||||
zones: string[];
|
||||
search_source: SearchSource;
|
||||
search_distance: number;
|
||||
};
|
||||
|
||||
export type SearchFilter = {
|
||||
|
@ -1,17 +1,26 @@
|
||||
import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import Chip from "@/components/indicators/Chip";
|
||||
import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog";
|
||||
import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Preview } from "@/types/preview";
|
||||
import { SearchFilter, SearchResult } from "@/types/search";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
import {
|
||||
LuExternalLink,
|
||||
LuImage,
|
||||
LuSearchCheck,
|
||||
LuSearchX,
|
||||
LuText,
|
||||
LuXCircle,
|
||||
} from "react-icons/lu";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -40,6 +49,15 @@ export default function SearchView({
|
||||
onUpdateFilter,
|
||||
onOpenSearch,
|
||||
}: SearchViewProps) {
|
||||
// remove duplicate event ids
|
||||
|
||||
const uniqueResults = useMemo(() => {
|
||||
return searchResults?.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex((v) => v.id === value.id),
|
||||
);
|
||||
}, [searchResults]);
|
||||
|
||||
// detail
|
||||
|
||||
const [searchDetail, setSearchDetail] = useState<SearchResult>();
|
||||
@ -57,6 +75,25 @@ export default function SearchView({
|
||||
[onOpenSearch],
|
||||
);
|
||||
|
||||
// confidence score - probably needs tweaking
|
||||
|
||||
const zScoreToConfidence = (score: number, source: string) => {
|
||||
let midpoint, scale;
|
||||
|
||||
if (source === "thumbnail") {
|
||||
midpoint = 2;
|
||||
scale = 0.5;
|
||||
} else {
|
||||
midpoint = 0.5;
|
||||
scale = 1.5;
|
||||
}
|
||||
|
||||
// Sigmoid function: 1 / (1 + e^x)
|
||||
const confidence = 1 / (1 + Math.exp((score - midpoint) * scale));
|
||||
|
||||
return Math.round(confidence * 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col pt-2 md:py-2">
|
||||
<Toaster closeButton={true} />
|
||||
@ -69,10 +106,12 @@ export default function SearchView({
|
||||
/>
|
||||
|
||||
<div className="relative mb-2 flex h-11 items-center justify-between pl-2 pr-2 md:pl-3">
|
||||
<div className="relative w-full md:w-1/3">
|
||||
<div className="relative mr-3 w-full md:w-1/3">
|
||||
<Input
|
||||
className="text-md w-full bg-muted pr-10"
|
||||
placeholder="Search for a specific detected object..."
|
||||
placeholder={
|
||||
isMobileOnly ? "Search" : "Search for a detected object..."
|
||||
}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
@ -124,8 +163,8 @@ export default function SearchView({
|
||||
)}
|
||||
|
||||
<div className="grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
|
||||
{searchResults &&
|
||||
searchResults.map((value) => {
|
||||
{uniqueResults &&
|
||||
uniqueResults.map((value) => {
|
||||
const selected = false;
|
||||
|
||||
return (
|
||||
@ -145,6 +184,34 @@ export default function SearchView({
|
||||
scrollLock={false}
|
||||
onClick={onSelectSearch}
|
||||
/>
|
||||
<div className={cn("absolute right-2 top-2 z-40")}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Chip
|
||||
className={`flex select-none items-center justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize text-white`}
|
||||
>
|
||||
{value.search_source == "thumbnail" ? (
|
||||
<LuImage className="mr-1 size-3" />
|
||||
) : (
|
||||
<LuText className="mr-1 size-3" />
|
||||
)}
|
||||
{zScoreToConfidence(
|
||||
value.search_distance,
|
||||
value.search_source,
|
||||
)}
|
||||
%
|
||||
</Chip>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Matched {value.search_source} at{" "}
|
||||
{zScoreToConfidence(
|
||||
value.search_distance,
|
||||
value.search_source,
|
||||
)}
|
||||
%
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-severity_alert outline-severity_alert` : "outline-transparent duration-500"}`}
|
||||
|
Loading…
Reference in New Issue
Block a user