"use client"; import dynamic from "next/dynamic"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; import { createStompClient } from "@/lib/chatSocket"; const API_BASE = ""; function AiChatPage() { const { user, token, loading: authLoading } = useAuth(); const router = useRouter(); const searchParams = useSearchParams(); const conversationIdParam = searchParams.get("id"); const [conversation, setConversation] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [loadingConv, setLoadingConv] = useState(true); const [error, setError] = useState(null); const [conversations, setConversations] = useState([]); const [convsLoading, setConvsLoading] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const messagesEndRef = useRef(null); const messagesAreaRef = useRef(null); const inputRef = useRef(null); const stompRef = useRef(null); const lastMessageIdRef = useRef(null); const fileInputRef = useRef(null); const lastScrolledIdRef = useRef(null); useEffect(() => { if (!authLoading && !user) { router.push("/login?next=" + encodeURIComponent("/ai-chat" + (conversationIdParam ? `?id=${conversationIdParam}` : ""))); } }, [authLoading, user, router, conversationIdParam]); useEffect(() => { if (messages.length === 0) return; const lastMsg = messages[messages.length - 1]; if (lastMsg.id === lastScrolledIdRef.current) return; lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; if (nearBottom) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [messages]); const fetchMessages = useCallback(async (convId) => { if (!token || !convId) return; try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return; const data = await res.json(); if (Array.isArray(data)) { setMessages(data); if (data.length > 0) lastMessageIdRef.current = data[data.length - 1].id; } } catch { // silent } }, [token]); const fetchConversation = useCallback(async (convId) => { if (!token || !convId) return null; try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return null; const data = await res.json(); setConversation(data); return data; } catch { return null; } }, [token]); const fetchConversations = useCallback(async () => { if (!token) return; setConvsLoading(true); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) return; const data = await res.json(); setConversations(Array.isArray(data) ? data : (data.content ?? [])); } catch { // silent } finally { setConvsLoading(false); } }, [token]); const connectStomp = useCallback((convId) => { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } const client = createStompClient(token); client.onConnect = () => { client.subscribe(`/topic/chat/conversations/${convId}`, (frame) => { try { const msg = JSON.parse(frame.body); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); lastMessageIdRef.current = msg.id; } catch { /* silent */ } }); const convTopic = user?.role === "CUSTOMER" ? `/user/queue/chat/conversations` : `/topic/chat/conversations`; client.subscribe(convTopic, (frame) => { try { const conv = JSON.parse(frame.body); if (conv.id === convId) setConversation(conv); setConversations((prev) => prev.map((c) => c.id === conv.id ? conv : c)); } catch { /* silent */ } }); }; stompRef.current = client; client.activate(); }, [token, user?.role]); useEffect(() => { if (!token || authLoading) return; async function init() { setLoadingConv(true); setError(null); let convId = conversationIdParam ? Number(conversationIdParam) : null; if (!convId) { try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { const list = await res.json(); const openAi = Array.isArray(list) ? list.find((c) => c.status === "OPEN" && c.mode === "AUTOMATED") : null; if (openAi) convId = openAi.id; } } catch { setError("Failed to load conversations."); } } if (!convId) { // Auto-create a new AI conversation try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ message: "Hello! I'd like to chat with the AI assistant." }), }); if (res.ok) { const conv = await res.json(); convId = conv.id; } } catch { // silent } } if (!convId) { await fetchConversations(); setLoadingConv(false); return; } await Promise.all([ fetchConversation(convId), fetchMessages(convId), fetchConversations(), ]); setLoadingConv(false); connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } init(); return () => { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } }; }, [token, authLoading, conversationIdParam, fetchConversation, fetchMessages, connectStomp, fetchConversations, router]); async function handleSend(e) { e?.preventDefault(); const text = input.trim(); if ((!text && !selectedFile) || sending || !conversation) return; if (selectedFile) { await handleSendAttachment(text); } else { await handleSendText(text); } } async function handleSendText(text) { setInput(""); setSending(true); setError(null); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${conversation.id}/messages`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ content: text }), }); if (res.status === 401) { router.push("/login?next=" + encodeURIComponent("/ai-chat")); return; } if (!res.ok) { const data = await res.json().catch(() => null); setError(data?.message || "Failed to send message."); setInput(text); return; } const msg = await res.json(); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); lastMessageIdRef.current = msg.id; } catch { setError("Network error. Please try again."); setInput(text); } finally { setSending(false); inputRef.current?.focus(); } } async function handleSendAttachment(optionalText) { setSending(true); setError(null); const file = selectedFile; setSelectedFile(null); setInput(""); try { const formData = new FormData(); formData.append("file", file); if (optionalText) formData.append("content", optionalText); const res = await fetch( `${API_BASE}/api/v1/chat/conversations/${conversation.id}/attachments`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: formData, } ); if (res.status === 401) { router.push("/login?next=" + encodeURIComponent("/ai-chat")); return; } if (!res.ok) { const data = await res.json().catch(() => null); setError(data?.message || "Failed to send attachment."); setSelectedFile(file); setInput(optionalText); return; } const msg = await res.json(); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); lastMessageIdRef.current = msg.id; } catch { setError("Network error. Please try again."); setSelectedFile(file); setInput(optionalText); } finally { setSending(false); inputRef.current?.focus(); } } function handleFileChange(e) { const file = e.target.files?.[0]; if (file) setSelectedFile(file); e.target.value = ""; } function handleKeyDown(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } } async function handleNewConversation() { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setError(null); setLoadingConv(true); try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ message: "Hello! I'd like to chat with the AI assistant." }), }); if (!res.ok) { const data = await res.json().catch(() => null); setError(data?.message || "Failed to start a conversation."); setLoadingConv(false); return; } const conv = await res.json(); setConversation(conv); await Promise.all([fetchMessages(conv.id), fetchConversations()]); setLoadingConv(false); connectStomp(conv.id); router.replace(`/ai-chat?id=${conv.id}`, { scroll: false }); } catch { setError("Network error. Please try again."); setLoadingConv(false); } } async function switchConversation(convId) { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setMessages([]); setError(null); setLoadingConv(true); await fetchConversation(convId); await fetchMessages(convId); setLoadingConv(false); connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } async function handleSwitchToHuman() { if (!conversation) return; try { await fetch(`${API_BASE}/api/v1/chat/conversations/${conversation.id}/request-human`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); router.push(`/chat?id=${conversation.id}`); } catch { setError("Could not connect to live support. Please try again."); } } if (authLoading || loadingConv) { return (

Loading...

); } if (!user) return null; const isEscalated = conversation?.mode === "HUMAN"; const isClosed = conversation?.status === "CLOSED"; return (

AI Pet Assistant

Ask me anything about pet care, adoption advice, or your pets!

All Conversations
{convsLoading &&

Loading...

} {!convsLoading && conversations.length === 0 && (

No conversations yet.

)}
{conversations.map((conv) => ( ))}
{!conversation ? (
🐾

No active conversation

Start a new conversation with the AI assistant.

{error &&
{error}
}
) : (
🐾
Leon's Pet Assistant
Online
{!isEscalated && !isClosed && ( )}
{messages.length === 0 && (
🐾

Hello{user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant. Ask me about pet recommendations, care tips, supplies, or anything pet-related!

)} {messages.map((msg) => { const isOwn = msg.senderId === user.id; return (
{!isOwn &&
🐾
}
{msg.content && msg.content.split("\n").map((line, i, arr) => ( {line} {i < arr.length - 1 &&
}
))} {msg.attachmentUrl && ( )}
{msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}
{isOwn && (
{user.fullName ? user.fullName.charAt(0).toUpperCase() : "U"}
)}
); })}
{error && (
{error}
)} {isClosed ? (
This conversation has been closed.
) : (
{selectedFile && (
📎 {selectedFile.name}
)}