mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-15 00:11:15 +03:00
Wire event attachments into chat composer and messages
This commit is contained in:
parent
d62ae17fcc
commit
01519607de
@ -3,16 +3,20 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { FaArrowUpLong } from "react-icons/fa6";
|
import { FaArrowUpLong } from "react-icons/fa6";
|
||||||
import { LuCircleAlert } from "react-icons/lu";
|
import { LuCircleAlert } from "react-icons/lu";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow";
|
import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow";
|
||||||
import { MessageBubble } from "@/components/chat/ChatMessage";
|
import { MessageBubble } from "@/components/chat/ChatMessage";
|
||||||
import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup";
|
import { ToolCallsGroup } from "@/components/chat/ToolCallsGroup";
|
||||||
import { ChatStartingState } from "@/components/chat/ChatStartingState";
|
import { ChatStartingState } from "@/components/chat/ChatStartingState";
|
||||||
|
import { ChatAttachmentChip } from "@/components/chat/ChatAttachmentChip";
|
||||||
|
import { ChatQuickReplies } from "@/components/chat/ChatQuickReplies";
|
||||||
|
import { ChatPaperclipButton } from "@/components/chat/ChatPaperclipButton";
|
||||||
import type { ChatMessage } from "@/types/chat";
|
import type { ChatMessage } from "@/types/chat";
|
||||||
import {
|
import {
|
||||||
getEventIdsFromSearchObjectsToolCalls,
|
getEventIdsFromSearchObjectsToolCalls,
|
||||||
getFindSimilarObjectsFromToolCalls,
|
getFindSimilarObjectsFromToolCalls,
|
||||||
|
prependAttachment,
|
||||||
streamChatCompletion,
|
streamChatCompletion,
|
||||||
} from "@/utils/chatUtil";
|
} from "@/utils/chatUtil";
|
||||||
|
|
||||||
@ -22,6 +26,7 @@ export default function ChatPage() {
|
|||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [attachedEventId, setAttachedEventId] = useState<string | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -75,12 +80,31 @@ export default function ChatPage() {
|
|||||||
[isLoading, t],
|
[isLoading, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendMessage = useCallback(() => {
|
const recentEventIds = useMemo(() => {
|
||||||
const text = input.trim();
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
if (!text || isLoading) return;
|
const msg = messages[i];
|
||||||
setInput("");
|
if (msg.role !== "assistant" || !msg.toolCalls) continue;
|
||||||
submitConversation([...messages, { role: "user", content: text }]);
|
const similar = getFindSimilarObjectsFromToolCalls(msg.toolCalls);
|
||||||
}, [input, isLoading, messages, submitConversation]);
|
if (similar) return similar.results.map((e) => e.id);
|
||||||
|
const events = getEventIdsFromSearchObjectsToolCalls(msg.toolCalls);
|
||||||
|
if (events.length > 0) return events.map((e) => e.id);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const sendMessage = useCallback(
|
||||||
|
(textOverride?: string) => {
|
||||||
|
const text = (textOverride ?? input).trim();
|
||||||
|
if (!text || isLoading) return;
|
||||||
|
const wireText = attachedEventId
|
||||||
|
? prependAttachment(text, attachedEventId)
|
||||||
|
: text;
|
||||||
|
setInput("");
|
||||||
|
setAttachedEventId(null);
|
||||||
|
submitConversation([...messages, { role: "user", content: wireText }]);
|
||||||
|
},
|
||||||
|
[attachedEventId, input, isLoading, messages, submitConversation],
|
||||||
|
);
|
||||||
|
|
||||||
const handleEditSubmit = useCallback(
|
const handleEditSubmit = useCallback(
|
||||||
(messageIndex: number, newContent: string) => {
|
(messageIndex: number, newContent: string) => {
|
||||||
@ -93,6 +117,10 @@ export default function ChatPage() {
|
|||||||
[messages, submitConversation],
|
[messages, submitConversation],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClearAttachment = useCallback(() => {
|
||||||
|
setAttachedEventId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full justify-center p-2 md:p-4">
|
<div className="flex size-full justify-center p-2 md:p-4">
|
||||||
<div className="flex size-full flex-col xl:w-[50%] 3xl:w-[35%]">
|
<div className="flex size-full flex-col xl:w-[50%] 3xl:w-[35%]">
|
||||||
@ -170,13 +198,19 @@ export default function ChatPage() {
|
|||||||
<ChatEventThumbnailsRow
|
<ChatEventThumbnailsRow
|
||||||
events={similar.results}
|
events={similar.results}
|
||||||
anchor={similar.anchor}
|
anchor={similar.anchor}
|
||||||
|
onAttach={setAttachedEventId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const events = getEventIdsFromSearchObjectsToolCalls(
|
const events = getEventIdsFromSearchObjectsToolCalls(
|
||||||
msg.toolCalls,
|
msg.toolCalls,
|
||||||
);
|
);
|
||||||
return <ChatEventThumbnailsRow events={events} />;
|
return (
|
||||||
|
<ChatEventThumbnailsRow
|
||||||
|
events={events}
|
||||||
|
onAttach={setAttachedEventId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -200,6 +234,10 @@ export default function ChatPage() {
|
|||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
placeholder={t("placeholder")}
|
placeholder={t("placeholder")}
|
||||||
|
attachedEventId={attachedEventId}
|
||||||
|
onClearAttachment={handleClearAttachment}
|
||||||
|
onAttach={setAttachedEventId}
|
||||||
|
recentEventIds={recentEventIds}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -210,9 +248,13 @@ export default function ChatPage() {
|
|||||||
type ChatEntryProps = {
|
type ChatEntryProps = {
|
||||||
input: string;
|
input: string;
|
||||||
setInput: (value: string) => void;
|
setInput: (value: string) => void;
|
||||||
sendMessage: () => void;
|
sendMessage: (textOverride?: string) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
attachedEventId: string | null;
|
||||||
|
onClearAttachment: () => void;
|
||||||
|
onAttach: (eventId: string) => void;
|
||||||
|
recentEventIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChatEntry({
|
function ChatEntry({
|
||||||
@ -221,6 +263,10 @@ function ChatEntry({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
isLoading,
|
isLoading,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
attachedEventId,
|
||||||
|
onClearAttachment,
|
||||||
|
onAttach,
|
||||||
|
recentEventIds,
|
||||||
}: ChatEntryProps) {
|
}: ChatEntryProps) {
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === "Enter" && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
@ -230,8 +276,28 @@ function ChatEntry({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex w-full flex-col items-center justify-center rounded-xl bg-secondary p-3">
|
<div className="mt-2 flex w-full flex-col items-stretch justify-center gap-2 rounded-xl bg-secondary p-3">
|
||||||
|
{attachedEventId && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ChatAttachmentChip
|
||||||
|
eventId={attachedEventId}
|
||||||
|
mode="composer"
|
||||||
|
onRemove={onClearAttachment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{attachedEventId && (
|
||||||
|
<ChatQuickReplies
|
||||||
|
onSend={(text) => sendMessage(text)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex w-full flex-row items-center gap-2">
|
<div className="flex w-full flex-row items-center gap-2">
|
||||||
|
<ChatPaperclipButton
|
||||||
|
recentEventIds={recentEventIds}
|
||||||
|
onAttach={onAttach}
|
||||||
|
disabled={isLoading || attachedEventId != null}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
className="w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
|
className="w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
@ -244,7 +310,7 @@ function ChatEntry({
|
|||||||
variant="select"
|
variant="select"
|
||||||
className="size-10 shrink-0 rounded-full"
|
className="size-10 shrink-0 rounded-full"
|
||||||
disabled={!input.trim() || isLoading}
|
disabled={!input.trim() || isLoading}
|
||||||
onClick={sendMessage}
|
onClick={() => sendMessage()}
|
||||||
>
|
>
|
||||||
<FaArrowUpLong size="16" />
|
<FaArrowUpLong size="16" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user