Files
frigate/web/src/components/chat/ChatEventThumbnailsRow.tsx
T
Josh Hawkins 98c2fe00c1 Chat improvements (#22823)
* Add score fusion helpers for find_similar_objects chat tool

* Add candidate query builder for find_similar_objects chat tool

* register find_similar_objects chat tool definition

* implement _execute_find_similar_objects chat tool dispatcher

* Dispatch find_similar_objects in chat tool executor

* Teach chat system prompt when to use find_similar_objects

* Add i18n strings for find_similar_objects chat tool

* Add frontend extractor for find_similar_objects tool response

* Render anchor badge and similarity scores in chat results

* formatting

* filter similarity results in python, not sqlite-vec

* extract pure chat helpers to chat_util module

* Teach chat system prompt about attached_event marker

* Add parseAttachedEvent and prependAttachment helpers

* Add i18n strings for chat event attachments

* Add ChatAttachmentChip component

* Make chat thumbnails attach to composer on click

* Render attachment chip in user chat bubbles

* Add ChatQuickReplies pill row component

* Add ChatPaperclipButton with event picker popover

* Wire event attachments into chat composer and messages

* add ability to stop streaming

* tweak cursor to appear at the end of the same line of the streaming response

* use abort signal

* add tooltip

* display label and camera on attachment chip
2026-04-09 14:31:37 -06:00

98 lines
3.0 KiB
TypeScript

import { useApiHost } from "@/api";
import { useTranslation } from "react-i18next";
import { LuExternalLink } from "react-icons/lu";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
type ChatEvent = { id: string; score?: number };
type ChatEventThumbnailsRowProps = {
events: ChatEvent[];
anchor?: { id: string } | null;
onAttach?: (eventId: string) => void;
};
/**
* Horizontal scroll row of event thumbnail images for chat.
* Optionally renders an anchor thumbnail with a "reference" badge above the
* results, and per-event similarity scores when provided.
* Clicking a thumbnail calls onAttach; a small external-link overlay opens
* the event in Explore.
* Renders nothing when there is nothing to show.
*/
export function ChatEventThumbnailsRow({
events,
anchor = null,
onAttach,
}: ChatEventThumbnailsRowProps) {
const apiHost = useApiHost();
const { t } = useTranslation(["views/chat"]);
if (events.length === 0 && !anchor) return null;
const renderThumb = (event: ChatEvent, isAnchor = false) => (
<div
key={event.id}
className={cn(
"relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg",
isAnchor && "ring-2 ring-primary",
)}
>
<button
type="button"
className="block size-full"
onClick={() => onAttach?.(event.id)}
aria-label={t("attach_event_aria", { eventId: event.id })}
>
<img
className="size-full object-cover"
src={`${apiHost}api/events/${event.id}/thumbnail.webp`}
alt=""
loading="lazy"
/>
</button>
<Tooltip>
<TooltipTrigger asChild>
<a
href={`/explore?event_id=${event.id}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="absolute right-1 top-1 flex size-6 items-center justify-center rounded bg-black/60 text-white hover:bg-black/80"
aria-label={t("open_in_explore")}
>
<LuExternalLink className="size-3" />
</a>
</TooltipTrigger>
<TooltipContent>{t("open_in_explore")}</TooltipContent>
</Tooltip>
{isAnchor && (
<span className="pointer-events-none absolute left-1 top-1 rounded bg-primary px-1 text-[10px] text-primary-foreground">
{t("anchor")}
</span>
)}
</div>
);
return (
<div className="flex min-w-0 max-w-full flex-col gap-2 self-start">
{anchor && (
<div className="scrollbar-container min-w-0 overflow-x-auto">
<div className="flex w-max gap-2">{renderThumb(anchor, true)}</div>
</div>
)}
{events.length > 0 && (
<div className="scrollbar-container min-w-0 overflow-x-auto">
<div className="flex w-max gap-2">
{events.map((event) => renderThumb(event))}
</div>
</div>
)}
</div>
);
}