frigate/web/src/pages/Chat.tsx

161 lines
4.9 KiB
TypeScript
Raw Normal View History

2026-02-13 03:57:38 +03:00
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { FaArrowUpLong } from "react-icons/fa6";
2026-02-13 05:01:22 +03:00
import { useTranslation } from "react-i18next";
import { useState, useCallback } from "react";
import axios from "axios";
import { AssistantMessage } from "@/components/chat/AssistantMessage";
import { ToolCallBubble } from "@/components/chat/ToolCallBubble";
import type { ChatMessage, ToolCall } from "@/types/chat";
2026-02-13 03:57:38 +03:00
export default function ChatPage() {
2026-02-13 05:01:22 +03:00
const { t } = useTranslation(["views/chat"]);
const [input, setInput] = useState("");
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const sendMessage = useCallback(async () => {
const text = input.trim();
if (!text || isLoading) return;
const userMessage: ChatMessage = { role: "user", content: text };
setInput("");
setError(null);
setMessages((prev) => [...prev, userMessage]);
setIsLoading(true);
try {
const apiMessages = [...messages, userMessage].map((m) => ({
role: m.role,
content: m.content,
}));
const { data } = await axios.post<{
message: { role: string; content: string | null };
2026-02-13 05:35:40 +03:00
tool_calls?: ToolCall[];
2026-02-13 05:01:22 +03:00
}>("chat/completion", { messages: apiMessages });
const content = data.message?.content ?? "";
setMessages((prev) => [
...prev,
2026-02-13 05:35:40 +03:00
{
role: "assistant",
content: content || " ",
toolCalls: data.tool_calls?.length ? data.tool_calls : undefined,
},
2026-02-13 05:01:22 +03:00
]);
} catch {
setError(t("error"));
} finally {
setIsLoading(false);
}
}, [input, isLoading, messages, t]);
2026-02-13 03:57:38 +03:00
return (
<div className="flex size-full flex-col items-center p-2">
2026-02-13 05:01:22 +03:00
<div className="flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto xl:w-[50%]">
{messages.map((msg, i) => (
<div key={i} className="flex flex-col gap-2">
{msg.role === "assistant" && msg.toolCalls && (
<>
{msg.toolCalls.map((tc, tcIdx) => (
<div key={tcIdx} className="flex flex-col gap-2">
<ToolCallBubble
name={tc.name}
arguments={tc.arguments}
side="left"
/>
{tc.response && (
<ToolCallBubble
name={tc.name}
response={tc.response}
side="right"
/>
)}
</div>
))}
</>
2026-02-13 05:19:22 +03:00
)}
<div
className={
msg.role === "user"
? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
: "self-start rounded-lg bg-muted px-3 py-2"
}
>
{msg.role === "assistant" ? (
<AssistantMessage content={msg.content} />
) : (
msg.content
)}
</div>
2026-02-13 05:01:22 +03:00
</div>
))}
2026-02-13 05:04:40 +03:00
{isLoading && (
<div className="self-start rounded-lg bg-muted px-3 py-2 text-muted-foreground">
{t("processing")}
</div>
)}
2026-02-13 05:01:22 +03:00
{error && (
<p className="self-start text-sm text-destructive" role="alert">
{error}
</p>
)}
</div>
<ChatEntry
input={input}
setInput={setInput}
sendMessage={sendMessage}
isLoading={isLoading}
placeholder={t("placeholder")}
/>
2026-02-13 03:57:38 +03:00
</div>
);
}
2026-02-13 05:01:22 +03:00
type ChatEntryProps = {
input: string;
setInput: (value: string) => void;
sendMessage: () => void;
isLoading: boolean;
placeholder: string;
};
function ChatEntry({
input,
setInput,
sendMessage,
isLoading,
placeholder,
}: ChatEntryProps) {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
2026-02-13 03:57:38 +03:00
return (
2026-02-13 05:01:22 +03:00
<div className="flex w-full flex-col items-center justify-center rounded-xl bg-secondary p-2 xl:w-[50%]">
<div className="flex w-full flex-row items-center gap-2">
<Input
className="w-full flex-1 border-transparent bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent"
placeholder={placeholder}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isLoading}
/>
<Button
variant="select"
className="size-10 shrink-0 rounded-full"
disabled={!input.trim() || isLoading}
onClick={sendMessage}
>
2026-02-13 03:57:38 +03:00
<FaArrowUpLong size="16" />
</Button>
</div>
</div>
);
}