mSnake / templates /index.html
Match01's picture
Create index.html
ab305ca verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alexa - Live Connect</title>
<style>
body { margin: 0; overflow: hidden; background-color: #0a0a0a; color: #00ff00; font-family: 'Courier New', Courier, monospace; }
canvas { display: block; }
#overlay-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 20; }
#start-button { font-size: 1.5em; padding: 15px 30px; background-color: #00ff00; color: #0a0a0a; border: none; cursor: pointer; font-family: 'Courier New', Courier, monospace; box-shadow: 0 0 20px rgba(0, 255, 0, 0.7); }
#start-button:hover { background-color: #55ff55; }
#status { margin-top: 20px; font-size: 1.2em; text-shadow: 0 0 5px #00ff00; }
#chat-container { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); width: 80%; max-width: 600px; z-index: 10; display: flex; flex-direction: column; align-items: center; visibility: hidden; /* Hidden by default */ }
#chat-input { width: 100%; padding: 10px; border: none; border-radius: 5px; background-color: rgba(50, 50, 70, 0.8); color: #00ff00; font-family: 'Courier New', Courier, monospace; font-size: 1em; outline: none; box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); }
#chat-input::placeholder { color: rgba(0, 255, 0, 0.5); }
</style>
</head>
<body>
<canvas id="three-canvas"></canvas>
<!-- Overlay for starting the session -->
<div id="overlay-container">
<button id="start-button">Click to Awaken Alexa</button>
<div id="status">Waiting to connect...</div>
</div>
<div id="chat-container">
<input type="text" id="chat-input" placeholder="Say something...">
</div>
<!-- Dependencies -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.158.0/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script>
<script>
// --- Global Variables ---
const canvas = document.getElementById('three-canvas');
const chatInput = document.getElementById('chat-input');
const startButton = document.getElementById('start-button');
const statusDiv = document.getElementById('status');
const overlay = document.getElementById('overlay-container');
const chatContainer = document.getElementById('chat-container');
let chatMessages = [];
const maxMessages = 10;
let scene, camera, renderer, terminalMesh, terminalTexture, textContext;
let mediaRecorder;
let audioContext;
let audioQueue = [];
let isPlaying = false;
// --- Socket.IO Connection ---
const socket = io();
// --- Core 3D Setup (from your original code, slightly adapted) ---
function initThree() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0a);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 3.5);
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
const ambientLight = new THREE.AmbientLight(0x404040, 2);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(directionalLight);
const terminalGeometry = new THREE.BoxGeometry(4, 2.5, 0.1);
const textCanvas = document.createElement('canvas');
textCanvas.width = 512;
textCanvas.height = 512;
textContext = textCanvas.getContext('2d');
terminalTexture = new THREE.CanvasTexture(textCanvas);
const terminalMaterial = new THREE.MeshStandardMaterial({
map: terminalTexture,
color: 0x222222,
emissive: 0x00ff00,
emissiveIntensity: 0.8,
metalness: 0.1,
roughness: 0.8,
});
terminalMesh = new THREE.Mesh(terminalGeometry, terminalMaterial);
terminalMesh.position.set(0, 1.5, 0);
scene.add(terminalMesh);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false);
animate();
addMessageToTerminal("System: Initializing...");
}
function animate() {
requestAnimationFrame(animate);
terminalMesh.rotation.y += 0.0005;
renderer.render(scene, camera);
}
function updateTerminalTexture() {
textContext.fillStyle = '#0a0a0a';
textContext.fillRect(0, 0, textContext.canvas.width, textContext.canvas.height);
textContext.font = 'Bold 30px Courier New';
textContext.fillStyle = '#00ff00';
textContext.textAlign = 'left';
textContext.textBaseline = 'top';
const padding = 20;
const lineHeight = 35;
let y = padding;
chatMessages.forEach(msg => {
textContext.fillText(msg, padding, y);
y += lineHeight;
});
terminalTexture.needsUpdate = true;
}
function addMessageToTerminal(message) {
chatMessages.push(message);
if (chatMessages.length > maxMessages) {
chatMessages.shift();
}
updateTerminalTexture();
}
// --- Web Audio and Media Recorder Setup ---
async function startAudioProcessing() {
// 1. Create AudioContext for playback (required by browsers to start after user interaction)
// The Gemini model sends audio at 24000 Hz.
audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 24000 });
// 2. Get microphone access
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// 3. Setup MediaRecorder to capture audio and send it to the server
// The `timeslice` parameter makes it emit data every 500ms.
mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
socket.emit('client_audio', event.data);
}
};
mediaRecorder.start(500); // Send data every 500ms
statusDiv.textContent = 'Connection active. Speak now!';
}
function playNextInQueue() {
if (isPlaying || audioQueue.length === 0) {
return;
}
isPlaying = true;
const audioData = audioQueue.shift();
// Decode the ArrayBuffer into an AudioBuffer
audioContext.decodeAudioData(audioData, (buffer) => {
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.onended = () => {
isPlaying = false;
playNextInQueue(); // Play the next chunk when this one is done
};
source.start();
}, (error) => {
console.error('Error decoding audio data:', error);
isPlaying = false;
playNextInQueue(); // Skip bad chunk
});
}
// --- Event Listeners ---
startButton.addEventListener('click', async () => {
statusDiv.textContent = "Connecting to Alexa...";
try {
// Initialize 3D scene and audio after user clicks
initThree();
await startAudioProcessing();
overlay.style.display = 'none'; // Hide the start button
chatContainer.style.visibility = 'visible'; // Show the chat input
} catch (error) {
console.error("Error starting session:", error);
statusDiv.textContent = "Error: Could not access microphone.";
addMessageToTerminal("Error: Mic access denied.");
}
});
chatInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
const message = chatInput.value.trim();
if (message) {
addMessageToTerminal("You: " + message);
socket.emit('client_text', { text: message });
chatInput.value = '';
}
}
});
// --- SocketIO Event Listeners ---
socket.on('connect', () => {
console.log('Socket.IO connected!');
statusDiv.textContent = 'Connected. Waiting for session...';
});
socket.on('session_ready', () => {
console.log('Gemini session is ready on the server.');
addMessageToTerminal('Alexa: Hey there! What\'s on your mind? ;)');
});
socket.on('server_text', (data) => {
console.log('Received text:', data.text);
addMessageToTerminal("Alexa: " + data.text);
});
socket.on('server_audio', (data) => {
// data is an ArrayBuffer. Add it to our queue for smooth playback.
audioQueue.push(data);
playNextInQueue();
});
socket.on('disconnect', () => {
console.log('Socket.IO disconnected.');
statusDiv.textContent = 'Disconnected.';
addMessageToTerminal("System: Connection lost.");
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
});
socket.on('error', (data) => {
console.error('Server error:', data.message);
statusDiv.textContent = `Error: ${data.message}`;
addMessageToTerminal(`System Error: ${data.message}`);
});
</script>
</body>
</html>