ZARAI / frontend /src /hooks /useStreamingChat.js
Srajal2579
Deploy JARVIS
3e646d9
Raw
History Blame Contribute Delete
3.98 kB
/**
* JARVIS β€” useStreamingChat hook
* Connects to the WebSocket streaming endpoint and manages
* real-time token delivery + tool use events.
*
* Usage:
* const { sendMessage, isStreaming, currentTokens, toolEvents } = useStreamingChat(sessionId)
*/
import { useState, useRef, useCallback, useEffect } from 'react'
const WS_BASE = import.meta.env.VITE_WS_URL ||
(window.location.protocol === 'https:' ? 'wss://' : 'ws://') +
window.location.host + '/api/ws'
export function useStreamingChat(sessionId) {
const [isStreaming, setIsStreaming] = useState(false)
const [currentTokens, setCurrentTokens] = useState('') // accumulates during stream
const [toolEvents, setToolEvents] = useState([])
const [error, setError] = useState(null)
const ws = useRef(null)
const onDoneRef = useRef(null)
// ── Connect / reconnect ─────────────────────────────────
const connect = useCallback(() => {
if (!sessionId) return
if (ws.current?.readyState === WebSocket.OPEN) return
const url = `${WS_BASE}/chat/${sessionId}`
ws.current = new WebSocket(url)
ws.current.onopen = () => {
setError(null)
}
ws.current.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
switch (msg.type) {
case 'thinking':
setCurrentTokens('...')
break
case 'token':
setCurrentTokens((prev) => prev === '...' ? msg.data : prev + msg.data)
break
case 'tool_use':
setToolEvents((prev) => [...prev, msg.data])
break
case 'done':
setIsStreaming(false)
if (onDoneRef.current) {
onDoneRef.current({
text: currentTokens,
used_tools: msg.data.used_tools || [],
memory_hits: msg.data.memory_hits || 0,
})
}
setCurrentTokens('')
setToolEvents([])
break
case 'error':
setError(msg.data)
setIsStreaming(false)
setCurrentTokens('')
break
default:
break
}
} catch (e) {
console.error('WS parse error:', e)
}
}
ws.current.onerror = () => {
setError('WebSocket connection error')
setIsStreaming(false)
}
ws.current.onclose = () => {
// Auto-reconnect after 2s if not intentionally closed
setTimeout(() => {
if (sessionId) connect()
}, 2000)
}
}, [sessionId])
// ── Connect on mount / sessionId change ─────────────────
useEffect(() => {
connect()
return () => {
ws.current?.close()
}
}, [connect])
// ── Send message ─────────────────────────────────────────
const sendMessage = useCallback(
(message, imageCaption = null, onDone = null) => {
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
connect()
// Retry after connection
setTimeout(() => sendMessage(message, imageCaption, onDone), 500)
return
}
setIsStreaming(true)
setCurrentTokens('')
setToolEvents([])
setError(null)
onDoneRef.current = onDone
ws.current.send(JSON.stringify({
message,
image_caption: imageCaption,
}))
},
[connect],
)
// ── Disconnect ───────────────────────────────────────────
const disconnect = useCallback(() => {
ws.current?.close()
}, [])
return {
sendMessage,
disconnect,
isStreaming,
currentTokens,
toolEvents,
error,
isConnected: ws.current?.readyState === WebSocket.OPEN,
}
}