Spaces:
Paused
Paused
| <!-- templates/index.html --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DevRaze - The Roastmaster AI</title> | |
| <script src="https://cdn.jsdelivr.net/npm/livekit-client/dist/livekit-client.umd.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap'); | |
| :root { | |
| --background-color: #121212; | |
| --text-color: #e0e0e0; | |
| --primary-color: #bb86fc; | |
| --primary-variant: #3700b3; | |
| --secondary-color: #03dac6; | |
| --surface-color: #1e1e1e; | |
| --error-color: #cf6679; | |
| --font-family: 'Roboto Mono', monospace; | |
| } | |
| body { | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| font-family: var(--font-family); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 100vh; | |
| margin: 0; | |
| text-align: center; | |
| } | |
| .container { | |
| background-color: var(--surface-color); | |
| padding: 40px; | |
| border-radius: 12px; | |
| box-shadow: 0 10px 20px rgba(0, 0, 0, 0.5); | |
| max-width: 500px; | |
| width: 90%; | |
| border: 1px solid var(--primary-variant); | |
| } | |
| h1 { | |
| color: var(--primary-color); | |
| margin-bottom: 10px; | |
| } | |
| p { | |
| margin-bottom: 30px; | |
| opacity: 0.8; | |
| line-height: 1.6; | |
| } | |
| #status { | |
| font-weight: bold; | |
| margin-bottom: 20px; | |
| padding: 10px; | |
| border-radius: 4px; | |
| transition: all 0.3s ease; | |
| word-break: break-word; /* Prevent long errors from breaking layout */ | |
| } | |
| .status-disconnected { | |
| color: var(--error-color); | |
| background-color: rgba(207, 102, 121, 0.1); | |
| } | |
| .status-connecting { | |
| color: #f0e68c; | |
| background-color: rgba(240, 230, 140, 0.1); | |
| } | |
| .status-connected { | |
| color: var(--secondary-color); | |
| background-color: rgba(3, 218, 198, 0.1); | |
| } | |
| button { | |
| background-color: var(--primary-color); | |
| color: #000; | |
| border: none; | |
| padding: 15px 30px; | |
| font-size: 16px; | |
| font-weight: bold; | |
| font-family: var(--font-family); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: background-color 0.3s ease, transform 0.2s ease; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| button:hover:not(:disabled) { | |
| background-color: var(--secondary-color); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4); | |
| } | |
| button:disabled { | |
| background-color: #444; | |
| color: #888; | |
| cursor: not-allowed; | |
| } | |
| /* Visualizer bars (optional enhancement) */ | |
| #visualizer { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| height: 30px; | |
| margin-top: 20px; | |
| gap: 3px; | |
| opacity: 0; | |
| transition: opacity 0.5s; | |
| } | |
| .bar { | |
| width: 4px; | |
| background-color: var(--secondary-color); | |
| border-radius: 2px; | |
| height: 5px; | |
| transition: height 0.1s ease; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>DevRaze, the Roastmaster AI</h1> | |
| <p>Embedded in Rajesh Yarra’s portfolio. Ask me anything about him. I dare you.</p> | |
| <div id="status" class="status-disconnected">Status: Ready to roast.</div> | |
| <button id="connect-button">Connect Microphone</button> | |
| <!-- Simple visualizer --> | |
| <div id="visualizer"> | |
| <div class="bar"></div><div class="bar"></div><div class="bar"></div> | |
| <div class="bar"></div><div class="bar"></div> | |
| </div> | |
| </div> | |
| <script> | |
| const connectButton = document.getElementById('connect-button'); | |
| const statusDiv = document.getElementById('status'); | |
| const visualizer = document.getElementById('visualizer'); | |
| const bars = document.querySelectorAll('.bar'); | |
| const TOKEN_ENDPOINT = '/get-token'; | |
| let room = null; | |
| let isConnected = false; | |
| function updateStatus(message, className) { | |
| statusDiv.textContent = `Status: ${message}`; | |
| statusDiv.className = className; | |
| } | |
| async function connectToRoom() { | |
| if (isConnected) return; | |
| connectButton.disabled = true; | |
| connectButton.textContent = "Initializing..."; | |
| updateStatus('Getting credentials...', 'status-connecting'); | |
| try { | |
| // 1. Fetch token and Livekit URL from our server | |
| const resp = await fetch(TOKEN_ENDPOINT); | |
| if (!resp.ok) { | |
| throw new Error(`Server error: ${resp.statusText}`); | |
| } | |
| const data = await resp.json(); | |
| if (!data.token || !data.livekitUrl) { | |
| throw new Error("Server didn't return token or URL"); | |
| } | |
| updateStatus('Connecting to Livekit...', 'status-connecting'); | |
| // 2. Create the room | |
| room = new LivekitClient.Room({ | |
| audioCaptureDefaults: { | |
| autoGainControl: true, | |
| echoCancellation: true, | |
| noiseSuppression: true, | |
| }, | |
| adaptiveStream: true, | |
| dynacast: true, | |
| }); | |
| // Setup Room Event Listeners | |
| room.on(LivekitClient.RoomEvent.Disconnected, () => { | |
| console.log('Disconnected from room'); | |
| handleDisconnect(); | |
| }); | |
| // Handle incoming audio from the Agent | |
| room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => { | |
| if (track.kind === LivekitClient.Track.Kind.Audio) { | |
| const element = track.attach(); | |
| document.body.appendChild(element); | |
| startVisualizer(track); // Optional: start simple visualizer | |
| } | |
| }); | |
| // 3. Connect to Livekit Cloud using the data from server | |
| await room.connect(data.livekitUrl, data.token); | |
| console.log('Successfully connected to room', room.name); | |
| updateStatus('Activating microphone...', 'status-connecting'); | |
| // 4. Publish local microphone | |
| await room.localParticipant.setMicrophoneEnabled(true); | |
| console.log('Microphone enabled'); | |
| // Update UI for connected state | |
| isConnected = true; | |
| updateStatus('Connected. Prepare to be roasted.', 'status-connected'); | |
| connectButton.textContent = "Disconnect"; | |
| connectButton.disabled = false; | |
| } catch (error) { | |
| console.error('Connection failed:', error); | |
| // Make error readable | |
| let errorMsg = error.message; | |
| if (errorMsg.includes('could not establish signal connection')) { | |
| errorMsg = "Could not connect to Livekit Cloud. Check server logs."; | |
| } | |
| updateStatus(`Error: ${errorMsg}`, 'status-disconnected'); | |
| connectButton.textContent = "Connect Microphone"; | |
| connectButton.disabled = false; | |
| handleDisconnect(); // Ensure cleanup | |
| } | |
| } | |
| async function handleDisconnect() { | |
| if (room) { | |
| await room.disconnect(); | |
| } | |
| room = null; | |
| isConnected = false; | |
| updateStatus('Disconnected', 'status-disconnected'); | |
| connectButton.textContent = "Connect Microphone"; | |
| connectButton.disabled = false; | |
| stopVisualizer(); | |
| } | |
| connectButton.addEventListener('click', () => { | |
| if (isConnected) { | |
| handleDisconnect(); | |
| } else { | |
| connectToRoom(); | |
| } | |
| }); | |
| // --- Simple Audio Visualizer (Optional) --- | |
| let audioContext, analyser, dataArray, visualizerFrame; | |
| function startVisualizer(track) { | |
| visualizer.style.opacity = 1; | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| } | |
| const stream = new MediaStream([track.mediaStreamTrack]); | |
| const source = audioContext.createMediaStreamSource(stream); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 32; | |
| source.connect(analyser); | |
| dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| animateVisualizer(); | |
| } | |
| function animateVisualizer() { | |
| if (!isConnected || !analyser) return; | |
| analyser.getByteFrequencyData(dataArray); | |
| // Map frequency data to the 5 bars | |
| const indices = [1, 3, 5, 7, 9]; // Pick some frequencies | |
| bars.forEach((bar, i) => { | |
| const value = dataArray[indices[i]] || 0; | |
| const height = Math.max(5, (value / 255) * 30); | |
| bar.style.height = `${height}px`; | |
| }); | |
| visualizerFrame = requestAnimationFrame(animateVisualizer); | |
| } | |
| function stopVisualizer() { | |
| visualizer.style.opacity = 0; | |
| if (visualizerFrame) cancelAnimationFrame(visualizerFrame); | |
| // Reset bars | |
| bars.forEach(bar => bar.style.height = '5px'); | |
| } | |
| </script> | |
| </body> | |
| </html> |