mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-18 06:08:22 +03:00
Add thumbnail images to object results
This commit is contained in:
parent
4bc1af8c37
commit
f2c60df4df
42
web/src/components/chat/ChatEventThumbnailsRow.tsx
Normal file
42
web/src/components/chat/ChatEventThumbnailsRow.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useApiHost } from "@/api";
|
||||||
|
|
||||||
|
type ChatEventThumbnailsRowProps = {
|
||||||
|
events: { id: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal scroll row of event thumbnail images for chat (e.g. after search_objects).
|
||||||
|
* Renders nothing when events is empty.
|
||||||
|
*/
|
||||||
|
export function ChatEventThumbnailsRow({
|
||||||
|
events,
|
||||||
|
}: ChatEventThumbnailsRowProps) {
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
|
||||||
|
if (events.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 max-w-full flex-col gap-1 self-start">
|
||||||
|
<div className="scrollbar-container min-w-0 overflow-x-auto">
|
||||||
|
<div className="flex w-max gap-2">
|
||||||
|
{events.map((event) => (
|
||||||
|
<a
|
||||||
|
key={event.id}
|
||||||
|
href={`/explore?event_id=${event.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="relative aspect-square size-32 shrink-0 overflow-hidden rounded-lg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="size-full object-cover"
|
||||||
|
src={`${apiHost}api/events/${event.id}/thumbnail.webp`}
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -4,10 +4,14 @@ import { FaArrowUpLong } from "react-icons/fa6";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow";
|
||||||
import { MessageBubble } from "@/components/chat/ChatMessage";
|
import { MessageBubble } from "@/components/chat/ChatMessage";
|
||||||
import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
|
import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
|
||||||
import type { ChatMessage } from "@/types/chat";
|
import type { ChatMessage } from "@/types/chat";
|
||||||
import { streamChatCompletion } from "@/utils/chatUtil";
|
import {
|
||||||
|
getEventIdsFromSearchObjectsToolCalls,
|
||||||
|
streamChatCompletion,
|
||||||
|
} from "@/utils/chatUtil";
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { t } = useTranslation(["views/chat"]);
|
const { t } = useTranslation(["views/chat"]);
|
||||||
@ -118,6 +122,15 @@ export default function ChatPage() {
|
|||||||
msg.role === "user" || !isLoading || i < messages.length - 1
|
msg.role === "user" || !isLoading || i < messages.length - 1
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{msg.role === "assistant" &&
|
||||||
|
(() => {
|
||||||
|
const isComplete = !isLoading || i < messages.length - 1;
|
||||||
|
if (!isComplete) return null;
|
||||||
|
const events = getEventIdsFromSearchObjectsToolCalls(
|
||||||
|
msg.toolCalls,
|
||||||
|
);
|
||||||
|
return <ChatEventThumbnailsRow events={events} />;
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -161,3 +161,33 @@ export async function streamChatCompletion(
|
|||||||
onDone();
|
onDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse search_objects tool call response(s) into event ids for thumbnails.
|
||||||
|
*/
|
||||||
|
export function getEventIdsFromSearchObjectsToolCalls(
|
||||||
|
toolCalls: ToolCall[] | undefined,
|
||||||
|
): { id: string }[] {
|
||||||
|
if (!toolCalls?.length) return [];
|
||||||
|
const results: { id: string }[] = [];
|
||||||
|
for (const tc of toolCalls) {
|
||||||
|
if (tc.name !== "search_objects" || !tc.response?.trim()) continue;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(tc.response) as unknown;
|
||||||
|
if (!Array.isArray(parsed)) continue;
|
||||||
|
for (const item of parsed) {
|
||||||
|
if (
|
||||||
|
item &&
|
||||||
|
typeof item === "object" &&
|
||||||
|
"id" in item &&
|
||||||
|
typeof (item as { id: unknown }).id === "string"
|
||||||
|
) {
|
||||||
|
results.push({ id: (item as { id: string }).id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user