From f400e91ededbfed35e526c9b327fa5a2ffa04e96 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 26 Feb 2026 08:36:52 -0700 Subject: [PATCH] Add a starting state for chat --- web/public/locales/en/views/chat.json | 13 +- web/src/components/chat/ChatStartingState.tsx | 89 +++++++++ web/src/pages/Chat.tsx | 172 ++++++++++-------- web/src/types/chat.ts | 5 + 4 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 web/src/components/chat/ChatStartingState.tsx diff --git a/web/public/locales/en/views/chat.json b/web/public/locales/en/views/chat.json index 3bd79a9b7..ec9e65e6e 100644 --- a/web/public/locales/en/views/chat.json +++ b/web/public/locales/en/views/chat.json @@ -1,4 +1,6 @@ { + "title": "Frigate Chat", + "subtitle": "Your AI assistant for camera management and insights", "placeholder": "Ask anything...", "error": "Something went wrong. Please try again.", "processing": "Processing...", @@ -9,5 +11,14 @@ "result": "Result", "arguments": "Arguments:", "response": "Response:", - "send": "Send" + "send": "Send", + "suggested_requests": "Try asking:", + "starting_requests": { + "show_recent_events": "Show recent events", + "show_camera_status": "Show camera status" + }, + "starting_requests_prompts": { + "show_recent_events": "Show me the recent events from the last hour", + "show_camera_status": "What is the current status of my cameras?" + } } diff --git a/web/src/components/chat/ChatStartingState.tsx b/web/src/components/chat/ChatStartingState.tsx new file mode 100644 index 000000000..2d0adaa2f --- /dev/null +++ b/web/src/components/chat/ChatStartingState.tsx @@ -0,0 +1,89 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { FaArrowUpLong } from "react-icons/fa6"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import type { StartingRequest } from "@/types/chat"; + +type ChatStartingStateProps = { + onSendMessage: (message: string) => void; +}; + +export function ChatStartingState({ onSendMessage }: ChatStartingStateProps) { + const { t } = useTranslation(["views/chat"]); + const [input, setInput] = useState(""); + + const defaultRequests: StartingRequest[] = [ + { + label: t("starting_requests.show_recent_events"), + prompt: t("starting_requests_prompts.show_recent_events"), + }, + { + label: t("starting_requests.show_camera_status"), + prompt: t("starting_requests_prompts.show_camera_status"), + }, + ]; + + const handleRequestClick = (prompt: string) => { + onSendMessage(prompt); + }; + + const handleSubmit = () => { + const text = input.trim(); + if (!text) return; + onSendMessage(text); + setInput(""); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+
+

{t("title")}

+

{t("subtitle")}

+
+ +
+

+ {t("suggested_requests")} +

+
+ {defaultRequests.map((request, idx) => ( + + ))} +
+
+ +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + /> + +
+
+ ); +} diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index ac812b58e..4acfc96de 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -7,6 +7,7 @@ import axios from "axios"; import { ChatEventThumbnailsRow } from "@/components/chat/ChatEventThumbnailsRow"; import { MessageBubble } from "@/components/chat/ChatMessage"; import { ToolCallBubble } from "@/components/chat/ToolCallBubble"; +import { ChatStartingState } from "@/components/chat/ChatStartingState"; import type { ChatMessage } from "@/types/chat"; import { getEventIdsFromSearchObjectsToolCalls, @@ -78,88 +79,101 @@ export default function ChatPage() { return (
-
- {messages.map((msg, i) => { - const isStreamingPlaceholder = - i === messages.length - 1 && - msg.role === "assistant" && - isLoading && - !msg.content?.trim() && - !(msg.toolCalls && msg.toolCalls.length > 0); - if (isStreamingPlaceholder) { - return
; - } - return ( -
- {msg.role === "assistant" && msg.toolCalls && ( - <> - {msg.toolCalls.map((tc, tcIdx) => ( -
- - {tc.response && ( + {messages.length === 0 ? ( + { + setInput(""); + submitConversation([{ role: "user", content: message }]); + }} + /> + ) : ( +
+ {messages.map((msg, i) => { + const isStreamingPlaceholder = + i === messages.length - 1 && + msg.role === "assistant" && + isLoading && + !msg.content?.trim() && + !(msg.toolCalls && msg.toolCalls.length > 0); + if (isStreamingPlaceholder) { + return
; + } + return ( +
+ {msg.role === "assistant" && msg.toolCalls && ( + <> + {msg.toolCalls.map((tc, tcIdx) => ( +
- )} -
- ))} - - )} - - {msg.role === "assistant" && - (() => { - const isComplete = !isLoading || i < messages.length - 1; - if (!isComplete) return null; - const events = getEventIdsFromSearchObjectsToolCalls( - msg.toolCalls, - ); - return ; - })()} -
- ); - })} - {(() => { - const lastMsg = messages[messages.length - 1]; - const showProcessing = - isLoading && - lastMsg?.role === "assistant" && - !lastMsg.content?.trim() && - !(lastMsg.toolCalls && lastMsg.toolCalls.length > 0); - return showProcessing ? ( -
- {t("processing")} -
- ) : null; - })()} - {error && ( -

- {error} -

- )} -
- + {tc.response && ( + + )} +
+ ))} + + )} + + {msg.role === "assistant" && + (() => { + const isComplete = !isLoading || i < messages.length - 1; + if (!isComplete) return null; + const events = getEventIdsFromSearchObjectsToolCalls( + msg.toolCalls, + ); + return ; + })()} +
+ ); + })} + {(() => { + const lastMsg = messages[messages.length - 1]; + const showProcessing = + isLoading && + lastMsg?.role === "assistant" && + !lastMsg.content?.trim() && + !(lastMsg.toolCalls && lastMsg.toolCalls.length > 0); + return showProcessing ? ( +
+ {t("processing")} +
+ ) : null; + })()} + {error && ( +

+ {error} +

+ )} +
+ )} + {messages.length > 0 && ( + + )}
); diff --git a/web/src/types/chat.ts b/web/src/types/chat.ts index b9217e1c2..8a1ea5443 100644 --- a/web/src/types/chat.ts +++ b/web/src/types/chat.ts @@ -9,3 +9,8 @@ export type ChatMessage = { content: string; toolCalls?: ToolCall[]; }; + +export type StartingRequest = { + label: string; + prompt: string; +};