"use client"; import { useChat } from "@ai-sdk/react"; import { useI18n } from "@/components/i18n/i18n-provider"; import { DefaultChatTransport, type UIMessage } from "ai"; import type { Translator } from "@/lib/i18n"; import { Brain, Loader2, MessageSquare, Plus, Send, Trash2 } from "lucide-react"; import { Button } from "poyraz-ui/atoms"; import { useEffect, useRef, useState } from "react"; import { toast } from "poyraz-ui/molecules"; import { createChatSessionAction, deleteChatSessionAction, listChatMessagesAction, listChatSessionsAction, } from "./actions"; function formatMessageContent(text: string) { if (!text) return null; const lines = text.split("\n"); return lines.map((line, i) => ( {line.split(/(\*\*.*?\*\*|\*.*?\*)/g).map((part, j) => { if (part.startsWith("**") && part.endsWith("**")) { return ( {part.slice(2, -2)} ); } if (part.startsWith("*") && part.endsWith("*")) { return {part.slice(1, -1)}; } return {part}; })} {i !== lines.length - 1 && } )); } type ChatSession = { id: string; title: string; created_at: string; }; export function AIChatClient({ locale }: { locale: string }) { const i18n = useI18n(); const t = i18n.t; const [sessions, setSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); const [input, setInput] = useState(""); const [isMobileSessionsOpen, setIsMobileSessionsOpen] = useState(false); const messagesEndRef = useRef(null); const { messages, sendMessage, setMessages, status, stop } = useChat({ transport: new DefaultChatTransport({ api: "/api/chat" }), onError: (error) => { console.error(error); toast.error(resolveChatError(t, error)); }, }); const isLoading = status === "submitted" || status === "streaming"; useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); useEffect(() => { async function fetchSessions() { try { const data = await listChatSessionsAction(); setSessions(data); setActiveSessionId(data[0]?.id || null); } catch (error) { toast.error(error instanceof Error ? error.message : t("chat.errors.loadSessions")); } } void fetchSessions(); }, [t]); useEffect(() => { async function fetchMessages() { if (!activeSessionId) { setMessages([]); return; } try { const data = await listChatMessagesAction(activeSessionId); const formattedMessages: UIMessage[] = data.map((message) => ({ id: message.id, role: message.role as UIMessage["role"], parts: [{ type: "text", text: message.content }], })); setMessages(formattedMessages); } catch (error) { toast.error(error instanceof Error ? error.message : t("chat.errors.loadMessages")); } } void fetchMessages(); }, [activeSessionId, setMessages, t]); async function handleNewChat() { setActiveSessionId(null); setMessages([]); } async function handleDeleteSession(id: string, event: React.MouseEvent) { event.stopPropagation(); try { await deleteChatSessionAction(id); } catch (error) { toast.error(error instanceof Error ? error.message : t("chat.errors.deleteSession")); return; } const nextSessions = sessions.filter((session) => session.id !== id); setSessions(nextSessions); if (activeSessionId === id) { setActiveSessionId(nextSessions[0]?.id || null); if (nextSessions.length === 0) setMessages([]); } } async function handleSubmit(event: { preventDefault: () => void }) { event.preventDefault(); const currentInput = input.trim(); if (!currentInput || isLoading) return; let sessionId = activeSessionId; setInput(""); if (!sessionId) { let newSession: ChatSession; try { newSession = await createChatSessionAction( currentInput.length > 32 ? `${currentInput.slice(0, 32)}...` : currentInput, ); } catch (error) { toast.error(error instanceof Error ? error.message : t("chat.errors.createSession")); setInput(currentInput); return; } sessionId = newSession.id; setActiveSessionId(sessionId); setSessions((currentSessions) => [newSession, ...currentSessions]); } await sendMessage({ text: currentInput }, { body: { sessionId, sourceLocale: locale } }); } const SessionsSidebarContent = ( <> {t("chat.sidebar.title")} { handleNewChat(); setIsMobileSessionsOpen(false); }}> {sessions.length === 0 ? ( {t("chat.sidebar.empty")} ) : ( sessions.map((session) => ( { setActiveSessionId(session.id); setIsMobileSessionsOpen(false); }} className="min-w-0 flex-1 justify-start px-3" > {session.title || t("chat.sidebar.untitled")} void handleDeleteSession(session.id, event)} className="text-destructive opacity-0 transition-opacity lg:group-hover:opacity-100" > )) )} > ); return ( {/* Desktop Sidebar */} {/* Mobile Sidebar Overlay */} {isMobileSessionsOpen && ( setIsMobileSessionsOpen(false)} /> )} {/* Mobile Sidebar Drawer */} {t("chat.title")} setIsMobileSessionsOpen(true)}> {t("chat.sidebar.title")} {messages.length === 0 ? ( {t("chat.empty.title")} {t("chat.empty.description")} ) : ( messages.map((message) => { const text = getMessageText(message); return ( {formatMessageContent(text)} ); }) )} {isLoading ? ( {t("chat.messages.loading")} ) : null} setInput(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void handleSubmit(event); } }} placeholder={t("chat.input.placeholder")} className="min-h-9 max-h-40 flex-1 resize-none bg-transparent px-2 py-2 text-sm outline-none placeholder:text-muted-foreground placeholder:truncate" rows={1} disabled={isLoading} /> {isLoading ? ( void stop()}> ) : ( )} ); } function resolveChatError(t: Translator["t"], error: unknown) { const message = error instanceof Error ? error.message : ""; if (!message) return t("chat.errors.communication"); const [key, detail] = message.split("|", 2); if (/^chat\.errors\./.test(key)) { const reasonKey = detail ? `chat.errorReasons.${detail}` : ""; const translatedDetail = reasonKey ? t(reasonKey) : ""; return t(key, { detail: translatedDetail && translatedDetail !== reasonKey ? translatedDetail : detail || t("chat.errors.noDetail"), }); } return message; } function getMessageText(message: UIMessage) { return message.parts .filter((part) => part.type === "text") .map((part) => part.text) .join(""); }
{t("chat.empty.description")}