LiveK / templates /index.html
SakibAhmed's picture
Upload 2 files
efd7f96 verified
<!-- templates/index.html -->
<!DOCTYPE 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>