Real-Time-Experts / index.html
jeremierostan's picture
Update index.html
5a218c9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Experts</title>
<style>
:root {
/* Base colors */
--color-default: #0066B3;
--color-behavior: #FCBA40;
--color-udl: #A50064;
--color-prompt3: #11C7B5;
--color-custom: #FF6B35;
--color-background: #FFFFFF;
--color-text: #333333;
--color-light-gray: #F5F5F5;
}
body {
margin: 0;
padding: 0;
background-color: var(--color-background);
color: var(--color-text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.app-title {
font-size: 2.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
transition: color 0.3s ease;
color: #333333;
}
.subtitle {
font-size: 1.125rem;
margin-bottom: 1rem;
color: #555555;
}
.api-link {
margin-bottom: 2rem;
transition: color 0.3s ease;
}
.mode-indicator {
position: absolute;
top: 15px;
right: 15px;
padding: 0.5rem 1rem;
border-radius: 2rem;
font-weight: 600;
color: white;
transition: all 0.3s ease;
}
/* Color themes */
body.theme-default .app-title,
body.theme-default a {
color: var(--color-default);
}
body.theme-default .box,
body.theme-default .start-button {
background-color: var(--color-default);
}
body.theme-default .mode-indicator {
background-color: var(--color-default);
}
body.theme-default .prompt-select {
border-color: var(--color-default);
}
body.theme-behavior .app-title,
body.theme-behavior a {
color: var(--color-behavior);
}
body.theme-behavior .box,
body.theme-behavior .start-button {
background-color: var(--color-behavior);
}
body.theme-behavior .mode-indicator {
background-color: var(--color-behavior);
color: #333333;
}
body.theme-behavior .prompt-select {
border-color: var(--color-behavior);
}
body.theme-udl .app-title,
body.theme-udl a {
color: var(--color-udl);
}
body.theme-udl .box,
body.theme-udl .start-button {
background-color: var(--color-udl);
}
body.theme-udl .mode-indicator {
background-color: var(--color-udl);
}
body.theme-udl .prompt-select {
border-color: var(--color-udl);
}
body.theme-prompt3 .app-title,
body.theme-prompt3 a {
color: var(--color-prompt3);
}
body.theme-prompt3 .box,
body.theme-prompt3 .start-button {
background-color: var(--color-prompt3);
}
body.theme-prompt3 .mode-indicator {
background-color: var(--color-prompt3);
}
body.theme-prompt3 .prompt-select {
border-color: var(--color-prompt3);
}
body.theme-custom .app-title,
body.theme-custom a {
color: var(--color-custom);
}
body.theme-custom .box,
body.theme-custom .start-button {
background-color: var(--color-custom);
}
body.theme-custom .mode-indicator {
background-color: var(--color-custom);
}
body.theme-custom .prompt-select {
border-color: var(--color-custom);
}
.container {
width: 90%;
max-width: 800px;
background-color: var(--color-background);
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: #555555;
}
.input-field {
width: calc(100% - 1.5rem);
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid #e0e0e0;
font-size: 1rem;
background-color: white;
}
.prompt-select {
width: 100%;
padding: 0.75rem;
border-radius: 0.5rem;
border-width: 2px;
border-style: solid;
font-size: 1rem;
background-color: white;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23555555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 1rem;
transition: border-color 0.3s ease;
}
.custom-prompt {
width: calc(100% - 1.5rem);
padding: 0.75rem;
min-height: 80px;
border-radius: 0.5rem;
border: 1px solid #e0e0e0;
font-size: 1rem;
font-family: inherit;
resize: vertical;
display: none;
}
.visualization {
height: 100px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 2rem 0;
padding: 1rem;
border-radius: 0.5rem;
background-color: var(--color-light-gray);
}
.box {
height: 80px;
width: 8px;
border-radius: 4px;
transition: transform 0.05s ease, background-color 0.3s ease;
}
.start-button {
display: inline-block;
padding: 0.75rem 2rem;
font-size: 1rem;
font-weight: 600;
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.3s ease;
}
.start-button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
background-color: #f44336;
color: white;
font-weight: 500;
z-index: 1000;
display: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid white;
border-top-color: transparent;
border-radius: 50%;
margin-right: 0.5rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
a {
text-decoration: none;
transition: color 0.3s ease;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body class="theme-default">
<div id="error-toast" class="toast"></div>
<div class="mode-indicator">Default Assistant Mode</div>
<div class="container">
<h1 class="app-title">Real-Time Experts</h1>
<p class="subtitle">Speak with Selected Expert Assistants</p>
<p class="api-link">Get a Gemini API key <a id="api-link" href="https://ai.google.dev/gemini-api/docs/api-key">here</a></p>
<div class="form-group">
<label for="api-key">API Key</label>
<input type="password" id="api-key" class="input-field" placeholder="Enter your API key">
</div>
<div class="form-group">
<label for="voice">Voice</label>
<select id="voice" class="input-field">
<option value="Puck">Puck</option>
<option value="Charon">Charon</option>
<option value="Kore">Kore</option>
<option value="Fenrir">Fenrir</option>
<option value="Aoede">Aoede</option>
</select>
</div>
<div class="form-group">
<label for="prompt-select">System Prompt</label>
<select id="prompt-select" class="prompt-select">
<option value="Default">Default Assistant</option>
<option value="Behavior Expert">Behavior Expert</option>
<option value="UDL Expert">UDL Expert</option>
<option value="Learning Support Expert">Learning Support Expert</option>
<option value="Custom">Custom Prompt</option>
</select>
<textarea id="custom-prompt" class="custom-prompt" placeholder="Enter custom instructions for the AI assistant"></textarea>
</div>
<div class="visualization" id="visualization">
<!-- Boxes will be dynamically added here -->
</div>
<button id="start-button" class="start-button">Start Conversation</button>
</div>
<audio id="audio-output"></audio>
<script>
// System prompts data injected from the server
const SYSTEM_PROMPTS = __SYSTEM_PROMPTS__;
// Theme configuration
const themeConfig = {
'Default': {
color: '#0066B3',
name: 'Default Assistant'
},
'Behavior Expert': {
color: '#FCBA40',
name: 'Behavior Expert'
},
'UDL Expert': {
color: '#A50064',
name: 'UDL Expert'
},
'Learning Support Expert': {
color: '#11C7B5',
name: 'Learning Support Expert'
},
'Custom': {
color: '#FF6B35',
name: 'Custom Assistant'
}
};
// Elements
const body = document.body;
const promptSelect = document.getElementById('prompt-select');
const customPrompt = document.getElementById('custom-prompt');
const visualization = document.getElementById('visualization');
const startButton = document.getElementById('start-button');
const modeIndicator = document.querySelector('.mode-indicator');
const appTitle = document.querySelector('.app-title');
// Create visualization bars
function createVisualizationBars() {
visualization.innerHTML = '';
const numBars = 30;
for (let i = 0; i < numBars; i++) {
const box = document.createElement('div');
box.className = 'box';
// Set initial scale
box.style.transform = `scaleY(0.4)`;
visualization.appendChild(box);
}
}
// Apply theme based on selected prompt
function applyTheme(promptKey) {
// Remove existing theme classes
body.className = '';
// Set the appropriate theme class
const themeClass = promptKey === 'Behavior Expert' ? 'theme-behavior' :
promptKey === 'UDL Expert' ? 'theme-udl' :
promptKey === 'Learning Support Expert' ? 'theme-prompt3' :
promptKey === 'Custom' ? 'theme-custom' : 'theme-default';
body.classList.add(themeClass);
// Update mode indicator
const config = themeConfig[promptKey] || themeConfig['Default'];
modeIndicator.textContent = config.name + ' Mode';
}
// Handle prompt selection change
promptSelect.addEventListener('change', function() {
const selectedValue = this.value;
// Toggle custom prompt textarea
if (selectedValue === 'Custom') {
customPrompt.style.display = 'block';
} else {
customPrompt.style.display = 'none';
}
// Apply theme
applyTheme(selectedValue);
});
// Initialize
createVisualizationBars();
applyTheme(promptSelect.value);
// Animation for visualization (simulated for demo)
function animateVisualization() {
const bars = document.querySelectorAll('.box');
bars.forEach(bar => {
const newScale = Math.random() * 0.8 + 0.2;
bar.style.transform = `scaleY(${newScale})`;
});
requestAnimationFrame(animateVisualization);
}
// Get the selected prompt data
function getSelectedPrompt() {
const selectedValue = promptSelect.value;
return {
promptKey: selectedValue === 'Custom' ? '' : selectedValue,
customPrompt: selectedValue === 'Custom' ? customPrompt.value : ''
};
}
let peerConnection;
let audioContext;
let dataChannel;
let isRecording = false;
let webrtc_id;
let analyser;
let dataArray;
let animationId;
const apiKeyInput = document.getElementById('api-key');
const voiceSelect = document.getElementById('voice');
const audioOutput = document.getElementById('audio-output');
function showError(message) {
const toast = document.getElementById('error-toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 5000);
}
function updateButtonState() {
if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
startButton.innerHTML = '<span class="spinner"></span>Connecting...';
startButton.disabled = true;
} else if (peerConnection && peerConnection.connectionState === 'connected') {
startButton.textContent = 'End Conversation';
startButton.disabled = false;
} else {
startButton.textContent = 'Start Conversation';
startButton.disabled = false;
}
}
async function setupWebRTC() {
const config = __RTC_CONFIGURATION__;
peerConnection = new RTCPeerConnection(config);
webrtc_id = Math.random().toString(36).substring(7);
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));
// Set up audio context for visualization
audioContext = new AudioContext();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(stream);
source.connect(analyser);
analyser.fftSize = 64;
dataArray = new Uint8Array(analyser.frequencyBinCount);
// Start visualization
updateVisualization();
// Connection state change listener
peerConnection.addEventListener('connectionstatechange', () => {
console.log('Connection state:', peerConnection.connectionState);
updateButtonState();
if (peerConnection.connectionState === 'connected') {
console.log('WebRTC connection established');
}
});
// Handle incoming audio
peerConnection.addEventListener('track', (evt) => {
if (audioOutput && audioOutput.srcObject !== evt.streams[0]) {
audioOutput.srcObject = evt.streams[0];
audioOutput.play();
}
});
// Data channel for messages
dataChannel = peerConnection.createDataChannel('text');
dataChannel.onmessage = (event) => {
const eventJson = JSON.parse(event.data);
if (eventJson.type === "error") {
showError(eventJson.message);
} else if (eventJson.type === "send_input") {
const promptData = getSelectedPrompt();
fetch('/input_hook', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
webrtc_id: webrtc_id,
api_key: apiKeyInput.value,
voice_name: voiceSelect.value,
prompt_key: promptData.promptKey,
custom_prompt: promptData.customPrompt
})
});
}
};
// Create and send offer
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// Wait for ICE gathering to complete
await new Promise((resolve) => {
if (peerConnection.iceGatheringState === "complete") {
resolve();
} else {
const checkState = () => {
if (peerConnection.iceGatheringState === "complete") {
peerConnection.removeEventListener("icegatheringstatechange", checkState);
resolve();
}
};
peerConnection.addEventListener("icegatheringstatechange", checkState);
}
});
// Send offer to server
const response = await fetch('/webrtc/offer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sdp: peerConnection.localDescription.sdp,
type: peerConnection.localDescription.type,
webrtc_id: webrtc_id,
})
});
const serverResponse = await response.json();
if (serverResponse.status === 'failed') {
showError(serverResponse.meta.error);
stopWebRTC();
return;
}
await peerConnection.setRemoteDescription(serverResponse);
} catch (err) {
console.error('Error setting up WebRTC:', err);
showError('Failed to establish connection. Please try again.');
stopWebRTC();
}
}
function updateVisualization() {
if (!analyser) return;
analyser.getByteFrequencyData(dataArray);
const bars = document.querySelectorAll('.box');
for (let i = 0; i < bars.length; i++) {
// Use data if available, otherwise animate randomly
if (i < dataArray.length) {
const barHeight = (dataArray[i] / 255) * 0.8 + 0.2;
bars[i].style.transform = `scaleY(${barHeight})`;
} else {
const barHeight = Math.random() * 0.8 + 0.2;
bars[i].style.transform = `scaleY(${barHeight})`;
}
}
animationId = requestAnimationFrame(updateVisualization);
}
function stopWebRTC() {
if (peerConnection) {
peerConnection.close();
}
if (animationId) {
cancelAnimationFrame(animationId);
}
if (audioContext) {
audioContext.close();
}
peerConnection = null;
updateButtonState();
}
startButton.addEventListener('click', () => {
if (!isRecording) {
setupWebRTC();
} else {
stopWebRTC();
}
isRecording = !isRecording;
});
// Start simple animation for initial state
//animateVisualization();
</script>
</body>
</html>