fix chat ux and ai model
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user