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";
|
2026-02-13 16:26:24 +03:00
|
|
|
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) => (
|
2026-02-13 16:26:24 +03:00
|
|
|
<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
|
|
|
)}
|
2026-02-13 16:26:24 +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>
|
|
|
|
|
);
|
|
|
|
|
}
|