From 04c560390534f94b4738c00484ab9889a41ad1a3 Mon Sep 17 00:00:00 2001 From: Guciolek <124672898+Guciolek@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:38:10 +0200 Subject: [PATCH] fix(chat): clear stuck 'typing...' cursor on empty/hung streaming responses (#228) --- frontend/src/App.jsx | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6268a53..4808dc5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -58,6 +58,17 @@ export default function App() { setStreaming(true); const aiMsg = { role: "assistant", content: "", sources: [], id: Date.now() + 1, streaming: true }; setMessages(prev => [...prev, aiMsg]); + // Safety timeout: if the server hangs or returns no tokens for 60s, + // force the "typing..." cursor away and show a fallback message. + // Prevents stuck UI on empty/hung streaming responses (issue #228). + const safetyTimeout = setTimeout(() => { + setMessages(prev => prev.map(m => + m.id === aiMsg.id && m.streaming + ? { ...m, content: m.content || "(empty response)", streaming: false } + : m + )); + setStreaming(false); + }, 60000); try { await api.streamMessage( { message: text, session_id: sessionId, model, use_documents: documents.length > 0, language }, @@ -69,7 +80,18 @@ export default function App() { ); } catch (e) { setMessages(prev => prev.map(m => m.id === aiMsg.id ? { ...m, content: e.message, streaming: false } : m)); - } finally { setStreaming(false); } + } finally { + clearTimeout(safetyTimeout); + // Safety net: if streaming is still true after the await returns, + // force-clear it so the cursor doesn't get stuck. This covers edge + // cases where the server returns 200 OK with an empty body. + setMessages(prev => prev.map(m => + m.id === aiMsg.id && m.streaming + ? { ...m, content: m.content || "(empty response)", streaming: false } + : m + )); + setStreaming(false); + } } else { setLoading(true); try {