diff --git a/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java b/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java index 026b9a3f..149bd4dc 100644 --- a/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java +++ b/backend/src/main/java/com/petshop/backend/service/OpenRouterAiService.java @@ -31,7 +31,7 @@ public class OpenRouterAiService { @Value("${openrouter.api-key:}") private String apiKey; - @Value("${openrouter.model:openai/gpt-oss-120b:free}") + @Value("${openrouter.model:google/gemma-4-31b-it:free}") private String model; private final String openRouterUrl = "https://openrouter.ai/api/v1/chat/completions"; diff --git a/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java b/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java index 6f851bfd..a70ece30 100644 --- a/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java +++ b/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java @@ -24,7 +24,7 @@ public class OpenRouterService { @Value("${openrouter.api-key:}") private String apiKey; - @Value("${openrouter.model:meta-llama/llama-3.3-70b-instruct:free}") + @Value("${openrouter.model:google/gemma-4-31b-it:free}") private String model; private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 6dbf101a..fc879dbc 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -84,6 +84,8 @@ function AiChatPage() { const [convsLoading, setConvsLoading] = useState(false); const [closedExpanded, setClosedExpanded] = useState(false); const [selectedFile, setSelectedFile] = useState(null); + const [botTyping, setBotTyping] = useState(false); + const [switchingConv, setSwitchingConv] = useState(false); const messagesEndRef = useRef(null); const messagesAreaRef = useRef(null); @@ -93,6 +95,7 @@ function AiChatPage() { const fileInputRef = useRef(null); const lastScrolledIdRef = useRef(null); const initialLoadDoneRef = useRef(false); + const botTypingTimeoutRef = useRef(null); useEffect(() => { if (!authLoading && !user) { @@ -108,11 +111,22 @@ function AiChatPage() { lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; - const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; - if (nearBottom) { + const isOwn = lastMsg.senderId === user?.id; + if (isOwn) { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + } else { + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; + if (nearBottom) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); } - }, [messages]); + }, [messages, user?.id]); + + useEffect(() => { + if (!botTyping) return; + const area = messagesAreaRef.current; + if (!area) return; + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; + if (nearBottom) messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [botTyping]); const fetchMessages = useCallback(async (convId) => { if (!token || !convId) return; @@ -183,6 +197,8 @@ function AiChatPage() { const msg = JSON.parse(frame.body); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); lastMessageIdRef.current = msg.id; + if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current); + setBotTyping(false); } catch { /* silent */ } }); const convTopic = user?.role === "CUSTOMER" @@ -310,6 +326,11 @@ function AiChatPage() { const msg = await res.json(); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); lastMessageIdRef.current = msg.id; + if (!isEscalated) { + setBotTyping(true); + if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current); + botTypingTimeoutRef.current = setTimeout(() => setBotTyping(false), 30000); + } } catch { setError("Network error. Please try again."); setInput(text); @@ -414,10 +435,11 @@ function AiChatPage() { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setMessages([]); setError(null); - setLoadingConv(true); + setBotTyping(false); + setSwitchingConv(true); await fetchConversation(convId); await fetchMessages(convId); - setLoadingConv(false); + setSwitchingConv(false); connectStomp(convId); router.replace(`/ai-chat?id=${convId}`, { scroll: false }); } @@ -629,6 +651,24 @@ function AiChatPage() { ); })} + + {botTyping && !isEscalated && ( +
+
🐾
+
+ + + +
+
+ )} + + {switchingConv && ( +
+ Loading messages… +
+ )} +
diff --git a/web/app/chat/page.js b/web/app/chat/page.js index dbee3ed3..3e243e8f 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -84,6 +84,7 @@ function ChatPage() { const [convsLoading, setConvsLoading] = useState(false); const [closedExpanded, setClosedExpanded] = useState(false); const [selectedFile, setSelectedFile] = useState(null); + const [switchingConv, setSwitchingConv] = useState(false); const messagesEndRef = useRef(null); const messagesAreaRef = useRef(null); @@ -108,11 +109,14 @@ function ChatPage() { lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; - const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; - if (nearBottom) { + const isOwn = lastMsg.senderId === user?.id; + if (isOwn) { area.scrollTop = area.scrollHeight; + } else { + const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; + if (nearBottom) area.scrollTop = area.scrollHeight; } - }, [messages]); + }, [messages, user?.id]); const fetchMessages = useCallback(async (convId) => { if (!token || !convId) return; @@ -416,10 +420,10 @@ function ChatPage() { if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } setMessages([]); setError(null); - setLoadingConv(true); + setSwitchingConv(true); await fetchConversation(convId); await fetchMessages(convId); - setLoadingConv(false); + setSwitchingConv(false); connectStomp(convId); router.replace(`/chat?id=${convId}`, { scroll: false }); } @@ -640,6 +644,11 @@ function ChatPage() { ); })} + {switchingConv && ( +
+ Loading messages… +
+ )}