'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ChatMessage } from '@shared/chat'; const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? 'http://localhost:4100'; const POLL_INTERVAL = 2500; export default function DashboardPage() { const { messages, refresh, error: pollError } = useChatMessages(); const { selection, status: overlayStatus } = useOverlaySelection(); const handleSelect = useCallback( async (message: ChatMessage) => { try { await fetch(`${BACKEND_URL}/overlay/selection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: message.id }) }); } catch (error) { console.error('Failed to select message', error); } }, [] ); const handleClear = useCallback(async () => { try { await fetch(`${BACKEND_URL}/overlay/selection`, { method: 'DELETE' }); } catch (error) { console.error('Failed to clear selection', error); } }, []); useEffect(() => { const timer = setInterval(() => { refresh(); }, POLL_INTERVAL); return () => clearInterval(timer); }, [refresh]); const statusHint = useMemo(() => { if (pollError) return 'Backend unreachable'; if (overlayStatus === 'connecting') return 'Connecting to overlay…'; if (overlayStatus === 'error') return 'Overlay stream disconnected'; return 'Live'; }, [pollError, overlayStatus]); return (

Operator Dashboard

Click a message to push it to the OBS overlay stream.

{statusHint}
Live Chat
{messages.map((message) => ( ))} {messages.length === 0 &&

Waiting for chat messages…

}
Overlay Preview
{selection ? (
{selection.author}

{selection.text}

) : (

No message selected yet.

)}
); } function useChatMessages() { const [messages, setMessages] = useState([]); const [error, setError] = useState(null); const refresh = useCallback(async () => { try { const response = await fetch(`${BACKEND_URL}/chat/messages`); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const data = await response.json(); setMessages(Array.isArray(data.messages) ? data.messages : []); setError(null); } catch (err) { setError(err as Error); } }, []); useEffect(() => { refresh(); }, [refresh]); return { messages, refresh, error }; } type OverlayStatus = 'connecting' | 'live' | 'error'; type SelectionPayload = { message: ChatMessage | null; }; function useOverlaySelection() { const [selection, setSelection] = useState(null); const [status, setStatus] = useState('connecting'); useEffect(() => { const source = new EventSource(`${BACKEND_URL}/overlay/stream`); const onSelection = (event: MessageEvent) => { try { const payload: SelectionPayload = JSON.parse(event.data); setSelection(payload.message); setStatus('live'); } catch (error) { console.error('Failed to parse selection payload', error); } }; source.addEventListener('selection', onSelection as EventListener); source.addEventListener('heartbeat', () => setStatus('live')); source.onerror = () => setStatus('error'); return () => { source.removeEventListener('selection', onSelection as EventListener); source.close(); }; }, []); return { selection, status }; }