* Object lifecycle and semantic search UI tweaks

* prevent console errors for sheet component
This commit is contained in:
Josh Hawkins 2024-09-09 09:33:38 -05:00 committed by GitHub
parent 8be139d4d1
commit f143fceceb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 174 additions and 47 deletions

View File

@ -379,7 +379,12 @@ def events_search():
n_results=limit, n_results=limit,
where=where, 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: else:
thumb_result = context.embeddings.thumbnail.query( thumb_result = context.embeddings.thumbnail.query(
query_texts=[query], query_texts=[query],

View File

@ -1,7 +1,19 @@
import { LogLine } from "@/types/log"; import { LogLine } from "@/types/log";
import { isDesktop } from "react-device-detect"; import { isDesktop } from "react-device-detect";
import { Sheet, SheetContent } from "../ui/sheet"; import {
import { Drawer, DrawerContent } from "../ui/drawer"; Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../ui/sheet";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "../ui/drawer";
import { LogChip } from "../indicators/Chip"; import { LogChip } from "../indicators/Chip";
import { useMemo } from "react"; import { useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -16,6 +28,9 @@ export default function LogInfoDialog({
}: LogInfoDialogProps) { }: LogInfoDialogProps) {
const Overlay = isDesktop ? Sheet : Drawer; const Overlay = isDesktop ? Sheet : Drawer;
const Content = isDesktop ? SheetContent : DrawerContent; 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); const helpfulLinks = useHelpfulLinks(logLine?.content);
@ -31,6 +46,10 @@ export default function LogInfoDialog({
<Content <Content
className={isDesktop ? "" : "max-h-[75dvh] overflow-hidden p-2 pb-4"} 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 && ( {logLine && (
<div className="flex size-full flex-col gap-5"> <div className="flex size-full flex-col gap-5">
<div className="flex w-min flex-col gap-1.5"> <div className="flex w-min flex-col gap-1.5">

View File

@ -44,6 +44,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { AnnotationSettingsPane } from "./AnnotationSettingsPane"; import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
import { TooltipPortal } from "@radix-ui/react-tooltip";
type ObjectLifecycleProps = { type ObjectLifecycleProps = {
review: ReviewSegment; review: ReviewSegment;
@ -185,7 +186,6 @@ export default function ObjectLifecycle({
if (!mainApi || !thumbnailApi) { if (!mainApi || !thumbnailApi) {
return; return;
} }
thumbnailApi.scrollTo(index);
mainApi.scrollTo(index); mainApi.scrollTo(index);
setCurrent(index); setCurrent(index);
}; };
@ -210,18 +210,10 @@ export default function ObjectLifecycle({
thumbnailApi.scrollTo(selected); thumbnailApi.scrollTo(selected);
}; };
const handleBottomSelect = () => { mainApi.on("select", handleTopSelect).on("reInit", handleTopSelect);
const selected = thumbnailApi.selectedScrollSnap();
setCurrent(selected);
mainApi.scrollTo(selected);
};
mainApi.on("select", handleTopSelect);
thumbnailApi.on("select", handleBottomSelect);
return () => { return () => {
mainApi.off("select", handleTopSelect); mainApi.off("select", handleTopSelect);
thumbnailApi.off("select", handleBottomSelect);
}; };
// we know that these deps are correct // we know that these deps are correct
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -467,15 +459,22 @@ export default function ObjectLifecycle({
<Carousel <Carousel
opts={{ opts={{
align: "center", align: "center",
containScroll: "keepSnaps",
dragFree: true,
}} }}
className="w-full max-w-[72%] md:max-w-[85%]" className="w-full max-w-[72%] md:max-w-[85%]"
setApi={setThumbnailApi} 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) => ( {eventSequence.map((item, index) => (
<CarouselItem <CarouselItem
key={index} 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)} onClick={() => handleThumbnailClick(index)}
> >
<div className="p-1"> <div className="p-1">
@ -486,15 +485,24 @@ export default function ObjectLifecycle({
index === current && "bg-selected", index === current && "bg-selected",
)} )}
> >
<LifecycleIcon <Tooltip>
className={cn( <TooltipTrigger>
"size-8", <LifecycleIcon
index === current className={cn(
? "bg-selected text-white" "size-8",
: "text-muted-foreground", index === current
)} ? "bg-selected text-white"
lifecycleItem={item} : "text-muted-foreground",
/> )}
lifecycleItem={item}
/>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="capitalize">
{getLifecycleItemDescription(item)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@ -1,6 +1,18 @@
import { isDesktop, isIOS, isMobile } from "react-device-detect"; import { isDesktop, isIOS, isMobile } from "react-device-detect";
import { Sheet, SheetContent } from "../../ui/sheet"; import {
import { Drawer, DrawerContent } from "../../ui/drawer"; Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../../ui/sheet";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "../../ui/drawer";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { useFormattedTimestamp } from "@/hooks/use-date-utils";
@ -66,6 +78,9 @@ export default function ReviewDetailDialog({
const Overlay = isDesktop ? Sheet : Drawer; const Overlay = isDesktop ? Sheet : Drawer;
const Content = isDesktop ? SheetContent : DrawerContent; const Content = isDesktop ? SheetContent : DrawerContent;
const Header = isDesktop ? SheetHeader : DrawerHeader;
const Title = isDesktop ? SheetTitle : DrawerTitle;
const Description = isDesktop ? SheetDescription : DrawerDescription;
if (!review) { if (!review) {
return; return;
@ -102,6 +117,10 @@ export default function ReviewDetailDialog({
: "max-h-[80dvh] overflow-hidden p-2 pb-4", : "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" && ( {pane == "overview" && (
<div className="scrollbar-container mt-3 flex size-full flex-col gap-5 overflow-y-auto"> <div className="scrollbar-container mt-3 flex size-full flex-col gap-5 overflow-y-auto">
<div className="flex w-full flex-row"> <div className="flex w-full flex-row">

View File

@ -1,6 +1,18 @@
import { isDesktop, isIOS } from "react-device-detect"; import { isDesktop, isIOS } from "react-device-detect";
import { Sheet, SheetContent } from "../../ui/sheet"; import {
import { Drawer, DrawerContent } from "../../ui/drawer"; Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../../ui/sheet";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "../../ui/drawer";
import { SearchResult } from "@/types/search"; import { SearchResult } from "@/types/search";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
@ -71,6 +83,9 @@ export default function SearchDetailDialog({
const Overlay = isDesktop ? Sheet : Drawer; const Overlay = isDesktop ? Sheet : Drawer;
const Content = isDesktop ? SheetContent : DrawerContent; const Content = isDesktop ? SheetContent : DrawerContent;
const Header = isDesktop ? SheetHeader : DrawerHeader;
const Title = isDesktop ? SheetTitle : DrawerTitle;
const Description = isDesktop ? SheetDescription : DrawerDescription;
return ( return (
<Overlay <Overlay
@ -86,6 +101,10 @@ export default function SearchDetailDialog({
isDesktop ? "sm:max-w-xl" : "max-h-[75dvh] overflow-hidden p-2 pb-4" 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 && ( {search && (
<div className="mt-3 flex size-full flex-col gap-5 md:mt-0"> <div className="mt-3 flex size-full flex-col gap-5 md:mt-0">
<div className="flex w-full flex-row"> <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="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Label</div> <div className="text-sm text-primary/40">Label</div>
<div className="flex flex-row items-center gap-2 text-sm capitalize"> <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} {search.label}
</div> </div>
</div> </div>

View File

@ -287,17 +287,6 @@ function PreviewContent({
/> />
); );
} else if (isCurrentHour(searchResult.start_time)) { } else if (isCurrentHour(searchResult.start_time)) {
return ( return <div />;
/*<InProgressPreview
review={review}
timeRange={timeRange}
setIgnoreClick={setIgnoreClick}
isPlayingBack={isPlayingBack}
onTimeUpdate={onTimeUpdate}
windowVisible={true}
setReviewed={() => { }}
/>*/
<div />
);
} }
} }

View File

@ -53,7 +53,7 @@ export default function Search() {
setTimeout(() => { setTimeout(() => {
setSearchTimeout(undefined); setSearchTimeout(undefined);
setSearchTerm(search); setSearchTerm(search);
}, 500), }, 750),
); );
// we only want to update the searchTerm when search changes // we only want to update the searchTerm when search changes
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -12,6 +12,7 @@ export type SearchResult = {
thumb_path?: string; thumb_path?: string;
zones: string[]; zones: string[];
search_source: SearchSource; search_source: SearchSource;
search_distance: number;
}; };
export type SearchFilter = { export type SearchFilter = {

View File

@ -1,17 +1,26 @@
import SearchFilterGroup from "@/components/filter/SearchFilterGroup"; import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import Chip from "@/components/indicators/Chip";
import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog"; import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog";
import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer"; import SearchThumbnailPlayer from "@/components/player/SearchThumbnailPlayer";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Preview } from "@/types/preview"; import { Preview } from "@/types/preview";
import { SearchFilter, SearchResult } from "@/types/search"; import { SearchFilter, SearchResult } from "@/types/search";
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import { isMobileOnly } from "react-device-detect";
import { import {
LuExternalLink, LuExternalLink,
LuImage,
LuSearchCheck, LuSearchCheck,
LuSearchX, LuSearchX,
LuText,
LuXCircle, LuXCircle,
} from "react-icons/lu"; } from "react-icons/lu";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -40,6 +49,15 @@ export default function SearchView({
onUpdateFilter, onUpdateFilter,
onOpenSearch, onOpenSearch,
}: SearchViewProps) { }: SearchViewProps) {
// remove duplicate event ids
const uniqueResults = useMemo(() => {
return searchResults?.filter(
(value, index, self) =>
index === self.findIndex((v) => v.id === value.id),
);
}, [searchResults]);
// detail // detail
const [searchDetail, setSearchDetail] = useState<SearchResult>(); const [searchDetail, setSearchDetail] = useState<SearchResult>();
@ -57,6 +75,25 @@ export default function SearchView({
[onOpenSearch], [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 ( return (
<div className="flex size-full flex-col pt-2 md:py-2"> <div className="flex size-full flex-col pt-2 md:py-2">
<Toaster closeButton={true} /> <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 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 <Input
className="text-md w-full bg-muted pr-10" 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} value={search}
onChange={(e) => setSearch(e.target.value)} 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"> <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 && {uniqueResults &&
searchResults.map((value) => { uniqueResults.map((value) => {
const selected = false; const selected = false;
return ( return (
@ -145,6 +184,34 @@ export default function SearchView({
scrollLock={false} scrollLock={false}
onClick={onSelectSearch} 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>
<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"}`} 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"}`}