import { setAgentState, setSpeechBubble, applyAgentStates } from './agents.js'; import { appendSystemMessage } from './ui.js'; function resolveWsUrl() { const explicit = import.meta.env.VITE_WS_URL; if (explicit) return explicit; const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${proto}//${window.location.host}/api/ws`; } const WS_URL = resolveWsUrl(); let ws = null; let connectionState = 'disconnected'; let jobCount = 0; let reconnectTimer = null; let visitorId = null; const RECONNECT_DELAY_MS = 5000; export function initWebSocket(_scene) { visitorId = crypto.randomUUID(); connect(); } function connect() { if (ws) { ws.onclose = null; ws.close(); } connectionState = 'connecting'; try { ws = new WebSocket(WS_URL); } catch { connectionState = 'disconnected'; scheduleReconnect(); return; } ws.onopen = () => { connectionState = 'connected'; clearTimeout(reconnectTimer); send({ type: 'visitor_enter', visitorId, visitorName: 'visitor' }); }; ws.onmessage = event => { try { handleMessage(JSON.parse(event.data)); } catch { /* ignore */ } }; ws.onerror = () => { connectionState = 'disconnected'; }; ws.onclose = () => { connectionState = 'disconnected'; scheduleReconnect(); }; } function scheduleReconnect() { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS); } function send(payload) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)); } } function handleMessage(msg) { switch (msg.type) { case 'ping': send({ type: 'pong' }); break; case 'world_state': { if (msg.agentStates) applyAgentStates(msg.agentStates); if (msg.recentEvents) { const last = msg.recentEvents.slice(-3); last.forEach(ev => appendSystemMessage(ev.summary || ev.type)); } break; } case 'timmy_state': { break; } case 'agent_state': { if (msg.agentId && msg.state) setAgentState(msg.agentId, msg.state); break; } case 'job_started': { jobCount++; if (msg.agentId) setAgentState(msg.agentId, 'active'); appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} started`); break; } case 'job_completed': { if (jobCount > 0) jobCount--; if (msg.agentId) setAgentState(msg.agentId, 'idle'); appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} complete`); break; } case 'chat': { if (msg.agentId === 'timmy') { // Timmy's AI reply: show in speech bubble + event log if (msg.text) setSpeechBubble(msg.text); appendSystemMessage('Timmy: ' + (msg.text || '').slice(0, 80)); } else if (msg.agentId === 'visitor') { // Another visitor's message: event log only (don't hijack the speech bubble) appendSystemMessage((msg.text || '').slice(0, 80)); } else { // System agent messages (delta payment confirmations, etc.): speech bubble if (msg.text) setSpeechBubble(msg.text); } break; } case 'agent_count': case 'visitor_count': break; default: break; } } export function sendVisitorMessage(text) { send({ type: 'visitor_message', visitorId, text }); } export function getConnectionState() { return connectionState; } export function getJobCount() { return jobCount; }