Add a starting state for chat

This commit is contained in:
Nicolas Mowen 2026-02-26 08:36:52 -07:00
parent 3bac4b15ae
commit f400e91ede
4 changed files with 199 additions and 80 deletions

View File

@ -1,4 +1,6 @@
{ {
"title": "Frigate Chat",
"subtitle": "Your AI assistant for camera management and insights",
"placeholder": "Ask anything...", "placeholder": "Ask anything...",
"error": "Something went wrong. Please try again.", "error": "Something went wrong. Please try again.",
"processing": "Processing...", "processing": "Processing...",
@ -9,5 +11,14 @@
"result": "Result", "result": "Result",
"arguments": "Arguments:", "arguments": "Arguments:",
"response": "Response:", "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?"
}
} }

View File

@ -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<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
};
return (
<div className="flex size-full flex-col items-center justify-center gap-6 p-8">
<div className="flex flex-col items-center gap-2">
<h1 className="text-4xl font-bold text-foreground">{t("title")}</h1>
<p className="text-muted-foreground">{t("subtitle")}</p>
</div>
<div className="flex w-full max-w-2xl flex-col items-center gap-4">
<p className="text-center text-sm text-muted-foreground">
{t("suggested_requests")}
</p>
<div className="flex w-full flex-wrap justify-center gap-2">
{defaultRequests.map((request, idx) => (
<Button
key={idx}
variant="outline"
className="max-w-sm text-sm"
onClick={() => handleRequestClick(request.prompt)}
>
{request.label}
</Button>
))}
</div>
</div>
<div className="flex w-full max-w-2xl flex-row items-center gap-2 rounded-xl bg-secondary p-4">
<Input
className="h-12 w-full flex-1 border-transparent bg-transparent text-base shadow-none focus-visible:ring-0 dark:bg-transparent"
placeholder={t("placeholder")}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
/>
<Button
variant="select"
className="size-10 shrink-0 rounded-full"
disabled={!input.trim()}
onClick={handleSubmit}
>
<FaArrowUpLong size="18" />
</Button>
</div>
</div>
);
}

View File

@ -7,6 +7,7 @@ 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 { ToolCallBubble } from "@/components/chat/ToolCallBubble"; import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
import { ChatStartingState } from "@/components/chat/ChatStartingState";
import type { ChatMessage } from "@/types/chat"; import type { ChatMessage } from "@/types/chat";
import { import {
getEventIdsFromSearchObjectsToolCalls, getEventIdsFromSearchObjectsToolCalls,
@ -78,6 +79,14 @@ export default function ChatPage() {
return ( return (
<div className="flex size-full justify-center p-2"> <div className="flex size-full justify-center p-2">
<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%]">
{messages.length === 0 ? (
<ChatStartingState
onSendMessage={(message) => {
setInput("");
submitConversation([{ role: "user", content: message }]);
}}
/>
) : (
<div className="scrollbar-container flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto"> <div className="scrollbar-container flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto">
{messages.map((msg, i) => { {messages.map((msg, i) => {
const isStreamingPlaceholder = const isStreamingPlaceholder =
@ -119,7 +128,9 @@ export default function ChatPage() {
msg.role === "user" ? handleEditSubmit : undefined msg.role === "user" ? handleEditSubmit : undefined
} }
isComplete={ isComplete={
msg.role === "user" || !isLoading || i < messages.length - 1 msg.role === "user" ||
!isLoading ||
i < messages.length - 1
} }
/> />
{msg.role === "assistant" && {msg.role === "assistant" &&
@ -153,6 +164,8 @@ export default function ChatPage() {
</p> </p>
)} )}
</div> </div>
)}
{messages.length > 0 && (
<ChatEntry <ChatEntry
input={input} input={input}
setInput={setInput} setInput={setInput}
@ -160,6 +173,7 @@ export default function ChatPage() {
isLoading={isLoading} isLoading={isLoading}
placeholder={t("placeholder")} placeholder={t("placeholder")}
/> />
)}
</div> </div>
</div> </div>
); );

View File

@ -9,3 +9,8 @@ export type ChatMessage = {
content: string; content: string;
toolCalls?: ToolCall[]; toolCalls?: ToolCall[];
}; };
export type StartingRequest = {
label: string;
prompt: string;
};