Download desktop/src/hooks/useCompanionWebSocket.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 4.57 kB
-
https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/hooks/useCompanionWebSocket.ts
- Command line
-
hf download hf://chenbhao/codev@e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/hooks/useCompanionWebSocket.ts
-
curl -L -o useCompanionWebSocket.ts https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/hooks/useCompanionWebSocket.ts
4.57 kB
| import { useRef, useCallback, useEffect } from 'react' | |
| import { useCompanionStore } from '../stores/companionStore' | |
| interface Callbacks { | |
| onAudioReceived: (data: ArrayBuffer) => void | |
| onDone?: () => void | |
| } | |
| export function useCompanionWebSocket(callbacks: Callbacks) { | |
| const wsRef = useRef<WebSocket | null>(null) | |
| const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) | |
| const reconnectAttemptRef = useRef(0) | |
| const callbacksRef = useRef(callbacks) | |
| const manualDisconnectRef = useRef(false) | |
| const serverUrl = useCompanionStore((s) => s.serverUrl) | |
| const status = useCompanionStore((s) => s.status) | |
| const setStatus = useCompanionStore((s) => s.setStatus) | |
| const setSpeaking = useCompanionStore((s) => s.setSpeaking) | |
| const setGenerating = useCompanionStore((s) => s.setGenerating) | |
| const appendTranscript = useCompanionStore((s) => s.appendTranscript) | |
| const appendFullTranscript = useCompanionStore((s) => s.appendFullTranscript) | |
| const resetTranscript = useCompanionStore((s) => s.resetTranscript) | |
| const setError = useCompanionStore((s) => s.setError) | |
| // Keep callbacks ref current | |
| callbacksRef.current = callbacks | |
| const cleanup = useCallback(() => { | |
| if (reconnectTimerRef.current) { | |
| clearTimeout(reconnectTimerRef.current) | |
| reconnectTimerRef.current = null | |
| } | |
| if (wsRef.current) { | |
| wsRef.current.onopen = null | |
| wsRef.current.onclose = null | |
| wsRef.current.onmessage = null | |
| wsRef.current.onerror = null | |
| wsRef.current.close() | |
| wsRef.current = null | |
| } | |
| }, []) | |
| const connect = useCallback(() => { | |
| manualDisconnectRef.current = false | |
| if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) { | |
| return | |
| } | |
| cleanup() | |
| setStatus('connecting') | |
| setError(null) | |
| resetTranscript() | |
| reconnectAttemptRef.current = 0 | |
| const ws = new WebSocket(serverUrl) | |
| ws.binaryType = 'arraybuffer' | |
| wsRef.current = ws | |
| ws.onopen = () => { | |
| setStatus('connected') | |
| } | |
| ws.onclose = () => { | |
| // No auto-reconnect — user clicks "call" to retry | |
| setStatus('disconnected') | |
| } | |
| ws.onerror = () => { | |
| setError('WebSocket connection error') | |
| } | |
| ws.onmessage = (event) => { | |
| if (event.data instanceof ArrayBuffer) { | |
| // Binary frame = PCM audio | |
| callbacksRef.current.onAudioReceived(event.data) | |
| } else { | |
| // Text frame = JSON control message | |
| try { | |
| const msg = JSON.parse(event.data) | |
| switch (msg.type) { | |
| case 'vad': | |
| setSpeaking(msg.speaking) | |
| break | |
| case 'generating': | |
| setGenerating(true) | |
| resetTranscript() | |
| break | |
| case 'text': | |
| appendTranscript(msg.content) | |
| break | |
| case 'done': | |
| appendFullTranscript( | |
| useCompanionStore.getState().transcript | |
| ) | |
| setGenerating(false) | |
| setSpeaking(false) | |
| callbacksRef.current.onDone?.() | |
| break | |
| } | |
| } catch { | |
| // Ignore malformed messages | |
| } | |
| } | |
| } | |
| }, [serverUrl, cleanup, setStatus, setError, resetTranscript, setSpeaking, setGenerating, appendTranscript, appendFullTranscript]) | |
| const disconnect = useCallback(() => { | |
| manualDisconnectRef.current = true | |
| cleanup() | |
| setStatus('disconnected') | |
| setError(null) | |
| }, [cleanup, setStatus]) | |
| const sendFrame = useCallback((base64Jpeg: string) => { | |
| if (wsRef.current?.readyState === WebSocket.OPEN) { | |
| wsRef.current.send(JSON.stringify({ type: 'frame', data: base64Jpeg })) | |
| } | |
| }, []) | |
| const sendAudio = useCallback((buffer: ArrayBuffer) => { | |
| if (wsRef.current?.readyState === WebSocket.OPEN) { | |
| wsRef.current.send(buffer) | |
| } | |
| }, []) | |
| const sendStop = useCallback(() => { | |
| if (wsRef.current?.readyState === WebSocket.OPEN) { | |
| wsRef.current.send(JSON.stringify({ type: 'stop' })) | |
| } | |
| }, []) | |
| const sendText = useCallback((text: string) => { | |
| if (wsRef.current?.readyState === WebSocket.OPEN) { | |
| wsRef.current.send(JSON.stringify({ type: 'text', content: text })) | |
| } | |
| }, []) | |
| // Cleanup on unmount | |
| useEffect(() => { | |
| return () => { | |
| manualDisconnectRef.current = true | |
| cleanup() | |
| } | |
| }, [cleanup]) | |
| return { | |
| connect, | |
| disconnect, | |
| sendFrame, | |
| sendAudio, | |
| sendStop, | |
| sendText, | |
| connected: status === 'connected', | |
| } | |
| } | |