import { useState, useCallback, useRef, useEffect } from 'react' import { sendChatMessage } from '../api/chat.js' import { parseSsePayload } from '../utils/sse.js' import { loadMessagesFromApi } from '../api/conversations.js' export function useMessages({ activeId, refreshConversations }) { const [messages, setMessages] = useState([]) const [isThinking, setIsThinking] = useState(false) const [statusText, setStatusText] = useState('') const bottomRef = useRef(null) const chunkedHistory = (msgs) => msgs.map((msg) => ({ role: msg.role, content: msg.content })) // Load messages from API when active conversation changes useEffect(() => { if (!activeId) return let cancelled = false loadMessagesFromApi(activeId).then((data) => { if (!cancelled) setMessages(data) }) return () => { cancelled = true } }, [activeId]) const sendMessage = useCallback(async ({ inputValue, uploadedImage, imageAnalysis, imageUrl, selectedTools, selectedDocuments, useRag, setInputValue, clearImageState, setShowTools, }) => { const trimmed = inputValue.trim() if ((!trimmed && !uploadedImage) || !activeId) return const history = chunkedHistory(messages) let messageContent = trimmed || (uploadedImage ? 'Please analyze the attached image' : '') const userMsg = { id: `user-${Date.now()}`, role: 'user', content: messageContent, ...(uploadedImage ? { image: uploadedImage, image_url: imageUrl } : {}), } setMessages((prev) => [...prev, userMsg, { id: `assistant-${Date.now()}`, role: 'assistant', content: '' }]) setInputValue('') clearImageState() setIsThinking(true) setStatusText('Thinking...') setShowTools(false) try { const reader = await sendChatMessage({ message: messageContent, history, conversationId: activeId, toolNames: selectedTools, documentIds: selectedDocuments, useRag, imageUrl, imageAnalysis, }) const decoder = new TextDecoder() let buffer = '' let receivedAnyEvent = false // Track whether timeout has fired — if so, the readLoop is still running // and will populate the message naturally when chunks arrive. let slowResponse = false // 90-second timeout: show friendly status instead of replacing message with "Error:" const SLOW_MS = 90000 const slowTimer = setTimeout(() => { slowResponse = true setStatusText(receivedAnyEvent ? 'Still working on it...' : 'Waiting for the response...') // Safety net: if still no data after another 2 minutes, show fallback text setTimeout(() => { setMessages((prev) => { const lastIndex = prev.length - 1 return prev.map((m, i) => i === lastIndex && m.role === 'assistant' && m.content === '' ? { ...m, content: 'The server did not respond. Please try again.' } : m, ) }) setIsThinking(false) setStatusText('') }, 120000) }, SLOW_MS) const readLoop = (async () => { while (true) { const { value, done } = await reader.read() if (done) break receivedAnyEvent = true buffer += decoder.decode(value, { stream: true }) const parts = buffer.split('\n\n') buffer = parts.pop() || '' for (const part of parts) { if (!part.trim()) continue const { event, data } = parseSsePayload(part) let payload = {} try { payload = JSON.parse(data) } catch { payload = { text: data } } if (event === 'status') { setStatusText(payload.state || 'Thinking...') } if (event === 'tool') { setStatusText(`Using tool: ${payload.tool || 'tool'}`) } if (event === 'chunk') { // If timeout previously fired, update status so user sees progress if (slowResponse) { setStatusText('Receiving response...') } setMessages((prev) => { const lastIndex = prev.length - 1 const content = prev[lastIndex]?.content || '' return prev.map((m, i) => i === lastIndex && m.role === 'assistant' ? { ...m, content: `${content}${payload.text || ''}` } : m, ) }) } if (event === 'done') { clearTimeout(slowTimer) setIsThinking(false) setStatusText('') refreshConversations() } } } })() await readLoop } catch (error) { // Only catch genuine errors (network failure, server error) — not timeout setMessages((prev) => prev.map((m) => m.role === 'assistant' && m.content === '' ? { ...m, content: `${error.message}` } : m, ), ) setIsThinking(false) setStatusText('') } }, [activeId, messages, refreshConversations]) return { messages, setMessages, isThinking, setIsThinking, statusText, setStatusText, bottomRef, sendMessage, } }