* 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,
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],

View File

@ -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">

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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 />;
}
}

View File

@ -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

View File

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

View File

@ -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"}`}