Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useRef, useCallback } from 'react'; | |
| /** | |
| * useChat — WebSocket chat hook for PC Pal | |
| * | |
| * Connects to /ws (proxied to ws://localhost:3001/ws by Vite), | |
| * sends an init message, and manages message state. | |
| * | |
| * @param {string|null} userId | |
| * @returns {{ messages, sendMessage, isConnected, isTyping }} | |
| */ | |
| export function useChat(userId) { | |
| const [messages, setMessages] = useState([]); | |
| const [isConnected, setIsConnected] = useState(false); | |
| const [isTyping, setIsTyping] = useState(false); | |
| const [connectionFailed, setConnectionFailed] = useState(false); | |
| const [activeSequence, setActiveSequence] = useState(null); | |
| const wsRef = useRef(null); | |
| const reconnectTimeoutRef = useRef(null); | |
| const messageIdRef = useRef(0); | |
| const reconnectAttemptsRef = useRef(0); | |
| const nextId = () => { | |
| messageIdRef.current += 1; | |
| return messageIdRef.current; | |
| }; | |
| const connect = useCallback(() => { | |
| if (!userId) return; | |
| // Determine WS URL: use relative path so Vite proxy handles it | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${protocol}//${window.location.host}/ws`; | |
| const ws = new WebSocket(wsUrl); | |
| wsRef.current = ws; | |
| ws.onopen = () => { | |
| setIsConnected(true); | |
| setConnectionFailed(false); | |
| reconnectAttemptsRef.current = 0; | |
| // Send init message so the server knows who we are | |
| ws.send(JSON.stringify({ type: 'init', userId })); | |
| }; | |
| ws.onmessage = (event) => { | |
| let data; | |
| try { | |
| data = JSON.parse(event.data); | |
| } catch { | |
| console.error('PC Pal: invalid JSON from server', event.data); | |
| return; | |
| } | |
| switch (data.type) { | |
| case 'init_ack': | |
| // Server acknowledged the init — nothing extra needed | |
| break; | |
| case 'typing': | |
| setIsTyping(true); | |
| break; | |
| case 'response': | |
| setIsTyping(false); | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: nextId(), | |
| role: 'assistant', | |
| text: data.text ?? '', | |
| timestamp: new Date().toISOString(), | |
| safetyAlert: data.safetyAlert ?? null, | |
| stepSequence: data.stepSequence || null, | |
| images: data.images || null, | |
| }, | |
| ]); | |
| if (data.stepSequence) { | |
| if (data.stepSequence.completed) { | |
| setActiveSequence(null); | |
| } else { | |
| setActiveSequence(data.stepSequence); | |
| } | |
| } | |
| break; | |
| case 'error': | |
| setIsTyping(false); | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: nextId(), | |
| role: 'assistant', | |
| text: data.message ?? 'Something went wrong. Please try again.', | |
| timestamp: new Date().toISOString(), | |
| safetyAlert: null, | |
| }, | |
| ]); | |
| break; | |
| default: | |
| console.warn('PC Pal: unknown message type', data.type); | |
| } | |
| }; | |
| ws.onclose = () => { | |
| setIsConnected(false); | |
| setIsTyping(false); | |
| const attempts = reconnectAttemptsRef.current; | |
| if (attempts >= 5) { | |
| setConnectionFailed(true); | |
| return; | |
| } | |
| reconnectAttemptsRef.current = attempts + 1; | |
| const delay = Math.min(1000 * Math.pow(2, attempts), 30000); | |
| reconnectTimeoutRef.current = setTimeout(connect, delay); | |
| }; | |
| ws.onerror = (err) => { | |
| console.error('PC Pal WebSocket error', err); | |
| ws.close(); | |
| }; | |
| }, [userId]); | |
| useEffect(() => { | |
| if (!userId) return; | |
| connect(); | |
| return () => { | |
| clearTimeout(reconnectTimeoutRef.current); | |
| if (wsRef.current) { | |
| // Remove the onclose handler before closing so we don't trigger reconnect | |
| wsRef.current.onclose = null; | |
| wsRef.current.close(); | |
| } | |
| }; | |
| }, [userId, connect]); | |
| /** | |
| * Send a chat message. | |
| * @param {string} text | |
| */ | |
| const sendMessage = useCallback( | |
| (text) => { | |
| const trimmed = text.trim(); | |
| if (!trimmed) return; | |
| // Optimistically add the user's message to the list | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: nextId(), | |
| role: 'user', | |
| text: trimmed, | |
| timestamp: new Date().toISOString(), | |
| safetyAlert: null, | |
| }, | |
| ]); | |
| if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { | |
| wsRef.current.send(JSON.stringify({ type: 'chat', text: trimmed })); | |
| } else { | |
| console.warn('PC Pal: WebSocket not open, message not sent'); | |
| setMessages((prev) => [ | |
| ...prev, | |
| { | |
| id: nextId(), | |
| role: 'assistant', | |
| text: 'Sorry, the connection was lost. Please refresh the page and try again.', | |
| timestamp: new Date().toISOString(), | |
| safetyAlert: null, | |
| }, | |
| ]); | |
| } | |
| }, | |
| [], | |
| ); | |
| return { messages, sendMessage, isConnected, isTyping, connectionFailed, activeSequence }; | |
| } | |