| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>EVE β Live</title> |
| <script src="https://cdn.jsdelivr.net/npm/livekit-client@2/dist/livekit-client.umd.js"></script> |
| <style> |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| background: #0a0a0f; |
| color: #e0e0e0; |
| font-family: system-ui, sans-serif; |
| height: 100vh; |
| display: grid; |
| grid-template-columns: 1fr 380px; |
| grid-template-rows: 1fr; |
| overflow: hidden; |
| } |
| |
| |
| .video-panel { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| background: #0a0a0f; |
| position: relative; |
| } |
| #video-container { |
| width: 100%; |
| max-width: 600px; |
| aspect-ratio: 1; |
| border-radius: 12px; |
| overflow: hidden; |
| border: 2px solid rgba(167,139,250,0.2); |
| box-shadow: 0 0 80px rgba(167,139,250,0.1); |
| background: #111; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| #video-container video { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| } |
| .video-status { |
| position: absolute; |
| top: 16px; |
| left: 16px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| font-size: 0.75rem; |
| color: #666; |
| background: rgba(0,0,0,0.6); |
| padding: 6px 12px; |
| border-radius: 20px; |
| } |
| .dot { width: 8px; height: 8px; border-radius: 50%; background: #666; } |
| .dot.live { background: #22c55e; animation: blink 2s infinite; } |
| @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} } |
| |
| .mic-controls { |
| position: absolute; |
| bottom: 20px; |
| display: flex; |
| gap: 12px; |
| align-items: center; |
| } |
| .mic-btn { |
| width: 56px; height: 56px; |
| border-radius: 50%; |
| border: none; |
| background: #4c1d95; |
| color: white; |
| font-size: 1.4rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| .mic-btn:hover { background: #5b21b6; transform: scale(1.05); } |
| .mic-btn.active { background: #dc2626; animation: pulse 1.5s infinite; } |
| @keyframes pulse { |
| 0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.4); } |
| 50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); } |
| } |
| .mic-label { font-size: 0.75rem; color: #888; } |
| |
| |
| .chat-panel { |
| display: flex; |
| flex-direction: column; |
| background: #111118; |
| border-left: 1px solid #222; |
| } |
| .chat-header { |
| padding: 16px 20px; |
| border-bottom: 1px solid #222; |
| text-align: center; |
| } |
| .chat-header h2 { |
| font-size: 1.3rem; |
| font-weight: 200; |
| letter-spacing: 0.3em; |
| color: #a78bfa; |
| } |
| .chat-header p { |
| font-size: 0.7rem; |
| color: #555; |
| margin-top: 4px; |
| } |
| |
| .chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 16px; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| .chat-messages::-webkit-scrollbar { width: 4px; } |
| .chat-messages::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; } |
| |
| .msg { |
| padding: 10px 14px; |
| border-radius: 12px; |
| max-width: 90%; |
| font-size: 0.85rem; |
| line-height: 1.5; |
| animation: fadeIn 0.3s ease; |
| } |
| @keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} } |
| |
| .msg.eve { |
| background: linear-gradient(135deg, #1e1b4b, #2a1a4e); |
| align-self: flex-start; |
| border: 1px solid #312e81; |
| color: #c4b5fd; |
| } |
| .msg.eve .sender { color: #a78bfa; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; } |
| .msg.user { |
| background: #1f2937; |
| align-self: flex-end; |
| border: 1px solid #374151; |
| color: #d1d5db; |
| } |
| .msg.user .sender { color: #9ca3af; font-size: 0.7rem; font-weight: 600; margin-bottom: 4px; text-align: right; } |
| .msg.system { |
| background: transparent; |
| align-self: center; |
| color: #555; |
| font-size: 0.75rem; |
| padding: 4px; |
| } |
| .msg.thinking { |
| background: linear-gradient(135deg, #1e1b4b, #2a1a4e); |
| align-self: flex-start; |
| border: 1px solid #312e81; |
| color: #7c6fc4; |
| font-style: italic; |
| } |
| |
| .chat-input { |
| display: flex; |
| gap: 8px; |
| padding: 12px 16px; |
| border-top: 1px solid #222; |
| background: #0d0d14; |
| } |
| .chat-input input { |
| flex: 1; |
| background: #1a1a24; |
| border: 1px solid #333; |
| border-radius: 8px; |
| padding: 10px 14px; |
| color: #e0e0e0; |
| font-size: 0.85rem; |
| outline: none; |
| } |
| .chat-input input:focus { border-color: #a78bfa; } |
| .chat-input button { |
| background: #4c1d95; |
| color: white; |
| border: none; |
| border-radius: 8px; |
| padding: 10px 18px; |
| cursor: pointer; |
| font-size: 0.85rem; |
| transition: background 0.2s; |
| } |
| .chat-input button:hover { background: #5b21b6; } |
| .chat-input button:disabled { opacity: 0.4; cursor: not-allowed; } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div class="video-panel"> |
| <div class="video-status"> |
| <span class="dot" id="live-dot"></span> |
| <span id="status-text">Connecting...</span> |
| </div> |
| <div id="video-container"> |
| <span style="color:#444">Waiting for stream...</span> |
| </div> |
| <div class="mic-controls"> |
| <button class="mic-btn" id="mic-btn" onclick="toggleMic()">π€</button> |
| <span class="mic-label" id="mic-label">Mic off</span> |
| </div> |
| </div> |
|
|
| |
| <div class="chat-panel"> |
| <div class="chat-header"> |
| <h2>E V E</h2> |
| <p>bitHuman Neural Avatar | LiveKit WebRTC</p> |
| </div> |
| <div class="chat-messages" id="messages"> |
| </div> |
| <div class="chat-input"> |
| <input type="text" id="chat-input" placeholder="Talk to Eve..." |
| onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendText()}"> |
| <button id="send-btn" onclick="sendText()">Send</button> |
| </div> |
| </div> |
|
|
| <script> |
| const LIVEKIT_URL = 'wss://tall-cotton-nvhnfg10.livekit.cloud'; |
| const GATEWAY_URL = 'http://localhost:8000'; |
| let room = null; |
| let micEnabled = false; |
| let thinkingDiv = null; |
| |
| |
| function addMessage(role, text) { |
| const container = document.getElementById('messages'); |
| const div = document.createElement('div'); |
| div.className = `msg ${role}`; |
| |
| const sender = document.createElement('div'); |
| sender.className = 'sender'; |
| sender.textContent = role === 'eve' ? 'Eve' : role === 'user' ? 'You' : ''; |
| |
| if (role !== 'system' && role !== 'thinking') div.appendChild(sender); |
| const body = document.createElement('div'); |
| body.textContent = text; |
| div.appendChild(body); |
| |
| container.appendChild(div); |
| container.scrollTop = container.scrollHeight; |
| return div; |
| } |
| |
| async function sendText() { |
| const input = document.getElementById('chat-input'); |
| const text = input.value.trim(); |
| if (!text || !room) return; |
| |
| input.value = ''; |
| addMessage('user', text); |
| |
| |
| thinkingDiv = addMessage('thinking', 'Eve is thinking...'); |
| |
| |
| const payload = JSON.stringify({ type: 'chat', text: text }); |
| const encoder = new TextEncoder(); |
| room.localParticipant.publishData(encoder.encode(payload), { reliable: true }); |
| } |
| |
| |
| async function getToken() { |
| const resp = await fetch(`${GATEWAY_URL}/livekit-token`); |
| return (await resp.json()).token; |
| } |
| |
| async function connectToEve() { |
| const statusText = document.getElementById('status-text'); |
| const liveDot = document.getElementById('live-dot'); |
| const container = document.getElementById('video-container'); |
| |
| try { |
| const token = await getToken(); |
| |
| room = new LivekitClient.Room({ |
| adaptiveStream: true, |
| dynacast: true, |
| audioCaptureDefaults: { |
| autoGainControl: true, |
| echoCancellation: true, |
| noiseSuppression: true, |
| }, |
| }); |
| |
| room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, pub, participant) => { |
| |
| if (participant.identity === room.localParticipant.identity) return; |
| if (track.kind === 'video') { |
| const el = track.attach(); |
| el.style.width = '100%'; |
| el.style.height = '100%'; |
| el.style.objectFit = 'cover'; |
| container.innerHTML = ''; |
| container.appendChild(el); |
| } |
| if (track.kind === 'audio') { |
| const el = track.attach(); |
| el.style.display = 'none'; |
| document.body.appendChild(el); |
| } |
| }); |
| |
| room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track) => { |
| track.detach().forEach(el => el.remove()); |
| }); |
| |
| |
| room.on(LivekitClient.RoomEvent.DataReceived, (data, participant) => { |
| try { |
| const decoder = new TextDecoder(); |
| const msg = JSON.parse(decoder.decode(data)); |
| if (msg.type === 'eve_response' && msg.text) { |
| |
| if (thinkingDiv) { |
| thinkingDiv.remove(); |
| thinkingDiv = null; |
| } |
| addMessage('eve', msg.text); |
| } |
| } catch (e) { |
| console.error('Data channel parse error:', e); |
| } |
| }); |
| |
| room.on(LivekitClient.RoomEvent.Connected, async () => { |
| statusText.textContent = 'Eve is live'; |
| liveDot.classList.add('live'); |
| |
| document.getElementById('mic-label').textContent = 'Click to talk'; |
| }); |
| |
| room.on(LivekitClient.RoomEvent.Disconnected, () => { |
| statusText.textContent = 'Disconnected'; |
| liveDot.classList.remove('live'); |
| addMessage('system', 'Disconnected'); |
| }); |
| |
| await room.connect(LIVEKIT_URL, token); |
| |
| } catch (err) { |
| statusText.textContent = `Error: ${err.message}`; |
| addMessage('system', `Connection error: ${err.message}`); |
| } |
| } |
| |
| async function enableMic() { |
| if (!room) return; |
| try { |
| await room.localParticipant.setMicrophoneEnabled(true); |
| micEnabled = true; |
| document.getElementById('mic-btn').classList.add('active'); |
| document.getElementById('mic-label').textContent = 'Mic live'; |
| } catch (err) { |
| document.getElementById('mic-label').textContent = 'Mic denied'; |
| } |
| } |
| |
| async function toggleMic() { |
| if (!room) return; |
| if (micEnabled) { |
| await room.localParticipant.setMicrophoneEnabled(false); |
| micEnabled = false; |
| document.getElementById('mic-btn').classList.remove('active'); |
| document.getElementById('mic-label').textContent = 'Mic off'; |
| } else { |
| await enableMic(); |
| } |
| } |
| |
| |
| window.addEventListener('load', () => setTimeout(connectToEve, 300)); |
| </script> |
| </body> |
| </html> |
|
|