fix chat ux and ai model

This commit is contained in:
2026-04-16 00:11:08 -06:00
parent 102edbdb19
commit 3c4ec5b11e
4 changed files with 61 additions and 12 deletions

View File

@@ -31,7 +31,7 @@ public class OpenRouterAiService {
@Value("${openrouter.api-key:}") @Value("${openrouter.api-key:}")
private String apiKey; private String apiKey;
@Value("${openrouter.model:openai/gpt-oss-120b:free}") @Value("${openrouter.model:google/gemma-4-31b-it:free}")
private String model; private String model;
private final String openRouterUrl = "https://openrouter.ai/api/v1/chat/completions"; private final String openRouterUrl = "https://openrouter.ai/api/v1/chat/completions";

View File

@@ -24,7 +24,7 @@ public class OpenRouterService {
@Value("${openrouter.api-key:}") @Value("${openrouter.api-key:}")
private String apiKey; 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 String model;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();

View File

@@ -84,6 +84,8 @@ function AiChatPage() {
const [convsLoading, setConvsLoading] = useState(false); const [convsLoading, setConvsLoading] = useState(false);
const [closedExpanded, setClosedExpanded] = useState(false); const [closedExpanded, setClosedExpanded] = useState(false);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const [botTyping, setBotTyping] = useState(false);
const [switchingConv, setSwitchingConv] = useState(false);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const messagesAreaRef = useRef(null); const messagesAreaRef = useRef(null);
@@ -93,6 +95,7 @@ function AiChatPage() {
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const lastScrolledIdRef = useRef(null); const lastScrolledIdRef = useRef(null);
const initialLoadDoneRef = useRef(false); const initialLoadDoneRef = useRef(false);
const botTypingTimeoutRef = useRef(null);
useEffect(() => { useEffect(() => {
if (!authLoading && !user) { if (!authLoading && !user) {
@@ -108,11 +111,22 @@ function AiChatPage() {
lastScrolledIdRef.current = lastMsg.id; lastScrolledIdRef.current = lastMsg.id;
const area = messagesAreaRef.current; const area = messagesAreaRef.current;
if (!area) return; if (!area) return;
const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 150; const isOwn = lastMsg.senderId === user?.id;
if (nearBottom) { if (isOwn) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); 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) => { const fetchMessages = useCallback(async (convId) => {
if (!token || !convId) return; if (!token || !convId) return;
@@ -183,6 +197,8 @@ function AiChatPage() {
const msg = JSON.parse(frame.body); const msg = JSON.parse(frame.body);
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
lastMessageIdRef.current = msg.id; lastMessageIdRef.current = msg.id;
if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current);
setBotTyping(false);
} catch { /* silent */ } } catch { /* silent */ }
}); });
const convTopic = user?.role === "CUSTOMER" const convTopic = user?.role === "CUSTOMER"
@@ -310,6 +326,11 @@ function AiChatPage() {
const msg = await res.json(); const msg = await res.json();
setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]); setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg]);
lastMessageIdRef.current = msg.id; lastMessageIdRef.current = msg.id;
if (!isEscalated) {
setBotTyping(true);
if (botTypingTimeoutRef.current) clearTimeout(botTypingTimeoutRef.current);
botTypingTimeoutRef.current = setTimeout(() => setBotTyping(false), 30000);
}
} catch { } catch {
setError("Network error. Please try again."); setError("Network error. Please try again.");
setInput(text); setInput(text);
@@ -414,10 +435,11 @@ function AiChatPage() {
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
setMessages([]); setMessages([]);
setError(null); setError(null);
setLoadingConv(true); setBotTyping(false);
setSwitchingConv(true);
await fetchConversation(convId); await fetchConversation(convId);
await fetchMessages(convId); await fetchMessages(convId);
setLoadingConv(false); setSwitchingConv(false);
connectStomp(convId); connectStomp(convId);
router.replace(`/ai-chat?id=${convId}`, { scroll: false }); router.replace(`/ai-chat?id=${convId}`, { scroll: false });
} }
@@ -629,6 +651,24 @@ function AiChatPage() {
</div> </div>
); );
})} })}
{botTyping && !isEscalated && (
<div style={{ ...s.messageRow, ...s.messageRowAgent }}>
<div style={s.aiAvatarSmall}>🐾</div>
<div style={{ ...s.messageBubble, ...s.bubbleAgent, display: "flex", alignItems: "center", gap: "4px", padding: "0.6rem 0.9rem" }}>
<span className="fc-dot" />
<span className="fc-dot" style={{ animationDelay: "0.2s" }} />
<span className="fc-dot" style={{ animationDelay: "0.4s" }} />
</div>
</div>
)}
{switchingConv && (
<div style={{ textAlign: "center", padding: "1rem", color: "#aaa", fontSize: "0.85rem" }}>
Loading messages
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>

View File

@@ -84,6 +84,7 @@ function ChatPage() {
const [convsLoading, setConvsLoading] = useState(false); const [convsLoading, setConvsLoading] = useState(false);
const [closedExpanded, setClosedExpanded] = useState(false); const [closedExpanded, setClosedExpanded] = useState(false);
const [selectedFile, setSelectedFile] = useState(null); const [selectedFile, setSelectedFile] = useState(null);
const [switchingConv, setSwitchingConv] = useState(false);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const messagesAreaRef = useRef(null); const messagesAreaRef = useRef(null);
@@ -108,11 +109,14 @@ function ChatPage() {
lastScrolledIdRef.current = lastMsg.id; lastScrolledIdRef.current = lastMsg.id;
const area = messagesAreaRef.current; const area = messagesAreaRef.current;
if (!area) return; if (!area) return;
const nearBottom = area.scrollHeight - area.scrollTop - area.clientHeight < 80; const isOwn = lastMsg.senderId === user?.id;
if (nearBottom) { if (isOwn) {
area.scrollTop = area.scrollHeight; 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) => { const fetchMessages = useCallback(async (convId) => {
if (!token || !convId) return; if (!token || !convId) return;
@@ -416,10 +420,10 @@ function ChatPage() {
if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; } if (stompRef.current) { stompRef.current.deactivate(); stompRef.current = null; }
setMessages([]); setMessages([]);
setError(null); setError(null);
setLoadingConv(true); setSwitchingConv(true);
await fetchConversation(convId); await fetchConversation(convId);
await fetchMessages(convId); await fetchMessages(convId);
setLoadingConv(false); setSwitchingConv(false);
connectStomp(convId); connectStomp(convId);
router.replace(`/chat?id=${convId}`, { scroll: false }); router.replace(`/chat?id=${convId}`, { scroll: false });
} }
@@ -640,6 +644,11 @@ function ChatPage() {
</div> </div>
); );
})} })}
{switchingConv && (
<div style={{ textAlign: "center", padding: "1rem", color: "#aaa", fontSize: "0.85rem" }}>
Loading messages
</div>
)}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</div> </div>