Soprano-TTS / UltimateAI.html
AptlyDigital's picture
Upload UltimateAI.html
635e476 verified
raw
history blame
73.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Tutor 3D Interface</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0a0f;
color: #fff;
font-family: 'Segoe UI', system-ui, sans-serif;
overflow: hidden;
height: 100vh;
}
#canvasContainer {
width: 100%;
height: 100%;
position: relative;
}
canvas {
display: block;
outline: none;
width: 100%;
height: 100%;
}
.ui-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
}
/* === CHAT INTERFACE === */
.chat-interface {
position: absolute;
top: 30px;
right: 30px;
width: 420px;
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
pointer-events: all;
z-index: 101;
opacity: 0;
transform: translateY(20px);
animation: slideIn 0.5s ease 1s forwards;
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
.chat-header {
background: rgba(20, 20, 30, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px 20px 0 0;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-title {
font-size: 1.2em;
font-weight: 300;
color: #a0b0ff;
display: flex;
align-items: center;
gap: 10px;
}
.chat-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85em;
color: #8892b0;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: #00ff9d;
animation: pulse 2s infinite;
}
.chat-container {
flex: 1;
background: rgba(20, 20, 30, 0.85);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-top: none;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
scrollbar-width: thin;
scrollbar-color: #5a6cff rgba(255, 255, 255, 0.1);
}
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #5a6cff;
border-radius: 3px;
}
.message {
max-width: 85%;
padding: 15px;
border-radius: 18px;
line-height: 1.5;
position: relative;
animation: messageAppear 0.3s ease;
}
@keyframes messageAppear {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-user {
align-self: flex-end;
background: linear-gradient(135deg, #5a6cff, #7a8aff);
color: white;
border-bottom-right-radius: 5px;
}
.message-ai {
align-self: flex-start;
background: rgba(255, 255, 255, 0.1);
color: #e0e0ff;
border-bottom-left-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.message-time {
font-size: 0.7em;
opacity: 0.7;
margin-top: 5px;
text-align: right;
}
.message-typing {
display: flex;
align-items: center;
gap: 5px;
padding: 15px;
background: rgba(255, 255, 255, 0.05);
border-radius: 18px;
border-bottom-left-radius: 5px;
align-self: flex-start;
width: 120px;
}
.typing-dot {
width: 8px;
height: 8px;
background: #a0b0ff;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
}
.chat-input-container {
padding: 20px;
background: rgba(15, 15, 25, 0.9);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.input-wrapper {
display: flex;
gap: 10px;
align-items: flex-end;
}
.chat-input {
flex: 1;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 15px 20px;
color: white;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 1em;
resize: none;
min-height: 56px;
max-height: 120px;
transition: all 0.3s ease;
}
.chat-input:focus {
outline: none;
border-color: #5a6cff;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(90, 108, 255, 0.2);
}
.chat-input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.voice-input-btn {
width: 56px;
height: 56px;
border-radius: 15px;
background: linear-gradient(135deg, #5a6cff, #7a8aff);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.voice-input-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(90, 108, 255, 0.4);
}
.voice-input-btn.listening {
background: linear-gradient(135deg, #ff5a5a, #ff7a7a);
animation: pulseListening 1.5s infinite;
}
@keyframes pulseListening {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 90, 90, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(255, 90, 90, 0); }
}
.send-btn {
width: 56px;
height: 56px;
border-radius: 15px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #a0b0ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.send-btn:hover {
background: rgba(90, 108, 255, 0.2);
color: white;
transform: translateY(-2px);
}
.voice-visualizer {
height: 40px;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
margin-top: 10px;
opacity: 0;
transition: opacity 0.3s ease;
}
.voice-visualizer.active {
opacity: 1;
}
.voice-bar {
width: 4px;
height: 10px;
background: #5a6cff;
border-radius: 2px;
transition: height 0.1s ease;
}
.voice-bar.listening {
background: #ff5a5a;
}
/* === VOICE CONTROLS PANEL === */
.voice-controls {
position: absolute;
bottom: 30px;
right: 470px;
background: rgba(20, 20, 30, 0.9);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 25px;
width: 280px;
pointer-events: all;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
z-index: 102;
opacity: 0;
transform: translateY(20px);
animation: slideIn 0.5s ease 1.2s forwards;
}
.voice-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
color: #a0b0ff;
font-size: 1.1em;
font-weight: 300;
}
.voice-control-group {
margin-bottom: 20px;
}
.voice-label {
display: block;
margin-bottom: 8px;
font-size: 0.9em;
color: #8892b0;
display: flex;
align-items: center;
gap: 8px;
}
.voice-select {
width: 100%;
padding: 12px 15px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: white;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 0.95em;
cursor: pointer;
transition: all 0.3s ease;
}
.voice-select:focus {
outline: none;
border-color: #5a6cff;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 0 2px rgba(90, 108, 255, 0.2);
}
.voice-select option {
background: #1a1a2a;
color: white;
padding: 10px;
}
.voice-slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.voice-slider {
flex: 1;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
.voice-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #5a6cff;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
.voice-slider::-webkit-slider-thumb:hover {
background: #7a8aff;
transform: scale(1.1);
}
.voice-value {
min-width: 40px;
text-align: center;
font-family: 'Courier New', monospace;
color: #5a6cff;
font-weight: 500;
font-size: 0.9em;
}
.voice-control-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 25px;
}
.voice-btn {
padding: 12px;
background: rgba(90, 108, 255, 0.1);
border: 1px solid rgba(90, 108, 255, 0.3);
color: #a0b0ff;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 0.9em;
}
.voice-btn:hover {
background: rgba(90, 108, 255, 0.2);
transform: translateY(-2px);
}
.voice-btn.active {
background: rgba(90, 108, 255, 0.3);
color: white;
}
.voice-btn.stop {
background: rgba(255, 90, 90, 0.1);
border-color: rgba(255, 90, 90, 0.3);
color: #ff9d9d;
}
.voice-btn.stop:hover {
background: rgba(255, 90, 90, 0.2);
}
.voice-btn.stop.active {
background: rgba(255, 90, 90, 0.3);
}
.voice-preview-btn {
width: 100%;
padding: 12px;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
color: #a0b0ff;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
font-size: 0.9em;
}
.voice-preview-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.voice-status {
display: flex;
align-items: center;
gap: 8px;
margin-top: 15px;
padding: 10px;
background: rgba(0, 255, 157, 0.1);
border-radius: 10px;
font-size: 0.85em;
color: #00ff9d;
border: 1px solid rgba(0, 255, 157, 0.2);
}
.voice-status i {
animation: pulse 2s infinite;
}
/* === EXISTING STYLES (PRESERVED) === */
.control-panel {
position: absolute;
bottom: 30px;
left: 30px;
background: rgba(20, 20, 30, 0.8);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 25px;
width: 300px;
pointer-events: all;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.control-title {
font-size: 1.2em;
margin-bottom: 20px;
font-weight: 300;
color: #a0b0ff;
display: flex;
align-items: center;
gap: 10px;
}
.control-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 0.9em;
color: #8892b0;
}
.slider-container {
display: flex;
align-items: center;
gap: 15px;
}
input[type="range"] {
flex: 1;
height: 6px;
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #5a6cff;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s;
}
input[type="range"]::-webkit-slider-thumb:hover {
background: #7a8aff;
transform: scale(1.1);
}
.value-display {
min-width: 40px;
text-align: center;
font-family: 'Courier New', monospace;
color: #5a6cff;
font-weight: 500;
}
.preset-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 20px;
}
.preset-btn {
padding: 10px;
background: rgba(90, 108, 255, 0.1);
border: 1px solid rgba(90, 108, 255, 0.3);
color: #a0b0ff;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9em;
text-align: center;
}
.preset-btn:hover {
background: rgba(90, 108, 255, 0.2);
transform: translateY(-2px);
}
.status-bar {
position: absolute;
top: 30px;
left: 30px;
background: rgba(20, 20, 30, 0.8);
backdrop-filter: blur(10px);
padding: 15px 25px;
border-radius: 15px;
font-size: 0.9em;
color: #8892b0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.pulse-indicator {
display: inline-block;
width: 12px;
height: 12px;
background: #00ff9d;
border-radius: 50%;
margin-right: 10px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.loading-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a0f;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
width: 60px;
height: 60px;
border: 3px solid rgba(90, 108, 255, 0.3);
border-top-color: #5a6cff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #0a0a0f;
display: none;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1001;
text-align: center;
padding: 20px;
}
.error-screen h2 {
color: #ff5a5a;
margin-bottom: 20px;
}
.error-screen p {
color: #8892b0;
margin-bottom: 30px;
max-width: 500px;
}
.retry-btn {
padding: 12px 30px;
background: #5a6cff;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1em;
transition: all 0.2s;
}
.retry-btn:hover {
background: #7a8aff;
transform: translateY(-2px);
}
/* Responsive adjustments */
@media (max-width: 1400px) {
.voice-controls {
right: 30px;
bottom: 400px;
}
.chat-interface {
width: 400px;
}
}
@media (max-height: 800px) {
.chat-interface {
height: calc(100vh - 80px);
}
.voice-controls {
padding: 20px;
width: 260px;
}
}
</style>
</head>
<body>
<div id="canvasContainer">
<canvas id="mainCanvas"></canvas>
<!-- Existing UI Overlay -->
<div class="ui-overlay">
<div class="status-bar">
<span class="pulse-indicator"></span>
<span id="statusText">AI Tutor System Ready</span>
</div>
</div>
<!-- Voice Controls Panel -->
<div class="voice-controls">
<div class="voice-header">
<i class="fas fa-robot"></i>
<span>Voice Synthesis</span>
</div>
<div class="voice-control-group">
<label class="voice-label">
<i class="fas fa-user-circle"></i>
Voice Selection
</label>
<select class="voice-select" id="voiceSelect">
<option value="">Loading voices...</option>
</select>
</div>
<div class="voice-control-group">
<label class="voice-label">
<i class="fas fa-tachometer-alt"></i>
Speech Rate
</label>
<div class="voice-slider-container">
<input type="range" class="voice-slider" id="rateSlider" min="0.5" max="2" step="0.1" value="1">
<span class="voice-value" id="rateValue">1.0x</span>
</div>
</div>
<div class="voice-control-group">
<label class="voice-label">
<i class="fas fa-wave-square"></i>
Pitch Variation
</label>
<div class="voice-slider-container">
<input type="range" class="voice-slider" id="pitchSlider" min="0.5" max="2" step="0.1" value="1">
<span class="voice-value" id="pitchValue">1.0</span>
</div>
</div>
<div class="voice-control-group">
<label class="voice-label">
<i class="fas fa-volume-up"></i>
Volume Level
</label>
<div class="voice-slider-container">
<input type="range" class="voice-slider" id="volumeSlider" min="0.1" max="1" step="0.1" value="0.8">
<span class="voice-value" id="volumeValue">80%</span>
</div>
</div>
<div class="voice-control-buttons">
<button class="voice-btn" id="autoSpeakBtn" data-enabled="true">
<i class="fas fa-bullhorn"></i>
Auto-Speak
</button>
<button class="voice-btn stop" id="stopBtn">
<i class="fas fa-stop"></i>
Stop
</button>
</div>
<button class="voice-preview-btn" id="previewBtn">
<i class="fas fa-play"></i>
Preview Voice
</button>
<div class="voice-status" id="voiceStatus" style="display: none;">
<i class="fas fa-comment-dots"></i>
<span id="statusMessage">Speaking...</span>
</div>
</div>
<!-- Chat Interface -->
<div class="chat-interface">
<div class="chat-header">
<div class="chat-title">
<i class="fas fa-robot"></i>
AI Tutor Assistant
</div>
<div class="chat-status">
<span class="status-indicator"></span>
<span id="chatStatusText">Online</span>
</div>
</div>
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<!-- Messages will be dynamically added here -->
<div class="message message-ai">
<div class="message-content">
Hello! I'm your AI Tutor. I can help explain concepts, answer questions, and guide your learning. How can I assist you today?
</div>
<div class="message-time">Just now</div>
</div>
<div class="message message-ai">
<div class="message-content">
You can type your questions or click the microphone icon to speak. I'll respond with explanations, examples, and follow-up questions to enhance your understanding.
</div>
<div class="message-time">Just now</div>
</div>
<!-- Typing indicator (hidden by default) -->
<div class="message-typing" id="typingIndicator" style="display: none;">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
<div class="chat-input-container">
<div class="input-wrapper">
<textarea
class="chat-input"
id="chatInput"
placeholder="Type your question or click the microphone to speak..."
rows="1"
></textarea>
<button class="voice-input-btn" id="voiceBtn" title="Start voice input">
<i class="fas fa-microphone"></i>
</button>
<button class="send-btn" id="sendBtn" title="Send message">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div class="voice-visualizer" id="voiceVisualizer">
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
<div class="voice-bar"></div>
</div>
</div>
</div>
</div>
<!-- Existing Control Panel -->
<div class="control-panel">
<div class="control-title">
<span>🧠 AI Tutor Interface</span>
</div>
<div class="control-group">
<label>Animation Intensity</label>
<div class="slider-container">
<input type="range" id="intensity" min="0" max="100" value="50">
<span class="value-display" id="intensityValue">50</span>
</div>
</div>
<div class="control-group">
<label>Particle Count</label>
<div class="slider-container">
<input type="range" id="particleCount" min="100" max="5000" value="2000" step="100">
<span class="value-display" id="particleCountValue">2000</span>
</div>
</div>
<div class="control-group">
<label>Energy Level</label>
<div class="slider-container">
<input type="range" id="energy" min="0" max="100" value="30">
<span class="value-display" id="energyValue">30</span>
</div>
</div>
<div class="preset-buttons">
<button class="preset-btn" data-preset="listening">🎤 Listening</button>
<button class="preset-btn" data-preset="processing">⚡ Processing</button>
<button class="preset-btn" data-preset="responding">💬 Responding</button>
<button class="preset-btn" data-preset="exploring">🔍 Exploring</button>
<button class="preset-btn" data-preset="teaching">📚 Teaching</button>
<button class="preset-btn" data-preset="idle">🌀 Idle</button>
</div>
</div>
<!-- Existing Loading Screen -->
<div class="loading-screen" id="loadingScreen">
<div class="spinner"></div>
<div id="loadingText">Initializing AI Interface...</div>
</div>
<!-- Existing Error Screen -->
<div class="error-screen" id="errorScreen">
<h2>⚠️ Initialization Failed</h2>
<p id="errorMessage">Three.js library failed to load. Please check your internet connection or try again.</p>
<button class="retry-btn" id="retryBtn">Retry</button>
</div>
</div>
<script>
// Check if Three.js loaded properly
function checkThreeJS() {
if (typeof THREE === 'undefined') {
console.error('Three.js failed to load');
showError('Three.js library failed to load. Please check your internet connection.');
return false;
}
return true;
}
function showError(message) {
const errorScreen = document.getElementById('errorScreen');
const errorMessage = document.getElementById('errorMessage');
const loadingScreen = document.getElementById('loadingScreen');
loadingScreen.style.display = 'none';
errorMessage.textContent = message;
errorScreen.style.display = 'flex';
}
function hideLoading() {
const loadingScreen = document.getElementById('loadingScreen');
loadingScreen.style.opacity = '0';
setTimeout(() => {
loadingScreen.style.display = 'none';
}, 500);
}
function updateStatus(text) {
document.getElementById('statusText').textContent = text;
document.getElementById('loadingText').textContent = text;
}
// Voice Synthesis System
class VoiceSynthesis {
constructor() {
this.synth = window.speechSynthesis;
this.voices = [];
this.currentVoice = null;
this.isAutoSpeak = true;
this.isSpeaking = false;
this.currentUtterance = null;
this.settings = {
rate: 1.0,
pitch: 1.0,
volume: 0.8
};
// Hardcoded longer, simpler test script for voice testing
this.testScript = "Hello there! This is a test to see how natural and fluid my voice sounds. I'm going to tell you a little story about learning and discovery. Imagine you're walking through a beautiful forest on a sunny day. The light filters through the leaves, creating patterns on the ground. Birds are singing in the trees, and there's a gentle breeze carrying the scent of flowers. You come across a small stream, and you sit on a rock to listen to the water flowing. It's in moments like these that we often have our best ideas and clearest thoughts. Learning is like this forest walk - it's a journey of discovery where each step reveals something new and wonderful. The key is to stay curious, keep exploring, and enjoy the process. Now, how does my voice sound to you? Is it clear, natural, and pleasant to listen to?";
this.initVoices();
this.setupEventListeners();
}
initVoices() {
// Wait for voices to be loaded
const loadVoices = () => {
this.voices = this.synth.getVoices();
this.populateVoiceList();
// Select a high-quality natural-sounding English voice
this.selectBestVoice();
};
if (this.synth.onvoiceschanged !== undefined) {
this.synth.onvoiceschanged = loadVoices;
}
// Load immediately if voices are already available
if (this.synth.getVoices().length > 0) {
loadVoices();
}
}
selectBestVoice() {
// Filter for English voices only
const englishVoices = this.voices.filter(voice =>
voice.lang.startsWith('en-')
);
if (englishVoices.length === 0) {
console.warn('No English voices found');
return;
}
// Prioritize natural-sounding voices (Google, Microsoft, Apple premium voices)
const preferredVoices = [
'Google UK English Male',
'Google US English',
'Microsoft David - English (United States)',
'Microsoft Zira - English (United States)',
'Alex',
'Samantha',
'Daniel',
'Karen',
'Moira',
'Tessa',
'Fred',
'Victoria'
];
// Try to find a preferred voice
for (const voiceName of preferredVoices) {
const voice = englishVoices.find(v => v.name.includes(voiceName));
if (voice) {
this.currentVoice = voice;
this.updateVoiceSelect(voice);
this.showStatus(`Selected: ${voice.name}`, 3000);
break;
}
}
// Fallback to first English voice
if (!this.currentVoice && englishVoices.length > 0) {
this.currentVoice = englishVoices[0];
this.updateVoiceSelect(englishVoices[0]);
this.showStatus(`Selected: ${englishVoices[0].name}`, 3000);
}
}
populateVoiceList() {
const voiceSelect = document.getElementById('voiceSelect');
voiceSelect.innerHTML = '';
// Filter for English voices only
const englishVoices = this.voices.filter(voice =>
voice.lang.startsWith('en-')
);
if (englishVoices.length === 0) {
const option = document.createElement('option');
option.textContent = 'No English voices found';
option.disabled = true;
voiceSelect.appendChild(option);
return;
}
// Sort English voices by dialect preference
const sortedVoices = englishVoices.sort((a, b) => {
const langOrder = ['en-US', 'en-GB', 'en-AU', 'en-CA', 'en-IN'];
const indexA = langOrder.indexOf(a.lang);
const indexB = langOrder.indexOf(b.lang);
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
return a.name.localeCompare(b.name);
});
// Add voices to select
sortedVoices.forEach(voice => {
const option = document.createElement('option');
option.value = voice.name;
option.textContent = `${voice.name} (${this.getDialectName(voice.lang)})`;
option.dataset.lang = voice.lang;
voiceSelect.appendChild(option);
});
}
getDialectName(langCode) {
const dialects = {
'en-US': 'US English',
'en-GB': 'UK English',
'en-AU': 'Australian English',
'en-CA': 'Canadian English',
'en-IN': 'Indian English',
'en-IE': 'Irish English',
'en-ZA': 'South African English',
'en-NZ': 'New Zealand English'
};
return dialects[langCode] || langCode;
}
updateVoiceSelect(voice) {
const voiceSelect = document.getElementById('voiceSelect');
const options = voiceSelect.querySelectorAll('option');
options.forEach(option => {
if (option.value === voice.name) {
option.selected = true;
}
});
}
setupEventListeners() {
// Voice selection
document.getElementById('voiceSelect').addEventListener('change', (e) => {
const selectedVoice = this.voices.find(v => v.name === e.target.value);
if (selectedVoice) {
this.currentVoice = selectedVoice;
this.showStatus(`Voice changed to: ${selectedVoice.name}`, 2000);
}
});
// Rate slider
document.getElementById('rateSlider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.settings.rate = value;
document.getElementById('rateValue').textContent = value.toFixed(1) + 'x';
});
// Pitch slider
document.getElementById('pitchSlider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.settings.pitch = value;
document.getElementById('pitchValue').textContent = value.toFixed(1);
});
// Volume slider
document.getElementById('volumeSlider').addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.settings.volume = value;
document.getElementById('volumeValue').textContent = Math.round(value * 100) + '%';
});
// Auto-speak toggle
document.getElementById('autoSpeakBtn').addEventListener('click', (e) => {
this.isAutoSpeak = !this.isAutoSpeak;
const btn = e.target.closest('.voice-btn');
btn.dataset.enabled = this.isAutoSpeak;
btn.innerHTML = this.isAutoSpeak
? '<i class="fas fa-bullhorn"></i> Auto-Speak'
: '<i class="fas fa-volume-mute"></i> Auto-Speak';
btn.classList.toggle('active', this.isAutoSpeak);
// Show status message
this.showStatus(this.isAutoSpeak ? 'Auto-speak enabled' : 'Auto-speak disabled', 2000);
});
// Stop button
document.getElementById('stopBtn').addEventListener('click', () => {
this.stopSpeaking();
});
// Preview button - uses the hardcoded test script
document.getElementById('previewBtn').addEventListener('click', () => {
this.previewCurrentVoice();
});
// Update status displays when speaking
this.synth.onstart = () => {
this.isSpeaking = true;
document.getElementById('stopBtn').classList.add('active');
this.showStatus('Speaking...');
// Update 3D visualization
if (window.visualization) {
window.visualization.setPreset('teaching');
}
};
this.synth.onend = () => {
this.isSpeaking = false;
document.getElementById('stopBtn').classList.remove('active');
this.hideStatus();
// Update 3D visualization
if (window.visualization) {
setTimeout(() => {
window.visualization.setPreset('idle');
}, 1000);
}
};
}
speak(text) {
if (!this.isAutoSpeak || !text || this.isSpeaking) return;
this.stopSpeaking();
const utterance = new SpeechSynthesisUtterance(text);
if (this.currentVoice) {
utterance.voice = this.currentVoice;
}
utterance.rate = this.settings.rate;
utterance.pitch = this.settings.pitch;
utterance.volume = this.settings.volume;
// Add natural pauses for better cadence
utterance.text = this.addNaturalPauses(text);
// Event listeners for this utterance
utterance.onerror = (event) => {
console.error('Speech synthesis error:', event);
this.hideStatus();
};
this.currentUtterance = utterance;
this.synth.speak(utterance);
}
addNaturalPauses(text) {
// Add slight pauses for commas and periods for more natural speech
return text
.replace(/,/g, ',<break time="200ms"/>')
.replace(/\./g, '.<break time="300ms"/>')
.replace(/\?/g, '?<break time="400ms"/>')
.replace(/\!/g, '!<break time="400ms"/>');
}
stopSpeaking() {
if (this.isSpeaking) {
this.synth.cancel();
this.isSpeaking = false;
this.currentUtterance = null;
document.getElementById('stopBtn').classList.remove('active');
this.hideStatus();
}
}
previewCurrentVoice() {
// Always use the hardcoded test script for voice testing
this.speak(this.testScript);
this.showStatus('Testing voice with story script...', 3000);
}
showStatus(message, duration = null) {
const statusEl = document.getElementById('voiceStatus');
const messageEl = document.getElementById('statusMessage');
messageEl.textContent = message;
statusEl.style.display = 'flex';
if (duration) {
setTimeout(() => {
this.hideStatus();
}, duration);
}
}
hideStatus() {
const statusEl = document.getElementById('voiceStatus');
statusEl.style.display = 'none';
}
updateSettingsFromUI() {
this.settings.rate = parseFloat(document.getElementById('rateSlider').value);
this.settings.pitch = parseFloat(document.getElementById('pitchSlider').value);
this.settings.volume = parseFloat(document.getElementById('volumeSlider').value);
}
}
// Chat Interface Functionality
class ChatInterface {
constructor() {
this.isListening = false;
this.voiceAnimationInterval = null;
this.initChat();
}
initChat() {
this.chatInput = document.getElementById('chatInput');
this.chatMessages = document.getElementById('chatMessages');
this.voiceBtn = document.getElementById('voiceBtn');
this.sendBtn = document.getElementById('sendBtn');
this.voiceVisualizer = document.getElementById('voiceVisualizer');
this.typingIndicator = document.getElementById('typingIndicator');
this.chatStatusText = document.getElementById('chatStatusText');
this.setupEventListeners();
this.autoResizeTextarea();
}
setupEventListeners() {
// Send button click
this.sendBtn.addEventListener('click', () => this.sendMessage());
// Enter key to send (Shift+Enter for new line)
this.chatInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// Voice button click
this.voiceBtn.addEventListener('click', () => this.toggleVoiceInput());
// Auto-resize textarea
this.chatInput.addEventListener('input', () => this.autoResizeTextarea());
}
autoResizeTextarea() {
const textarea = this.chatInput;
textarea.style.height = 'auto';
const newHeight = Math.min(textarea.scrollHeight, 120);
textarea.style.height = newHeight + 'px';
}
toggleVoiceInput() {
this.isListening = !this.isListening;
if (this.isListening) {
// Start "listening" state
this.voiceBtn.classList.add('listening');
this.voiceBtn.innerHTML = '<i class="fas fa-stop"></i>';
this.voiceVisualizer.classList.add('active');
this.chatStatusText.textContent = 'Listening...';
this.startVoiceAnimation();
// Update 3D visualization to listening mode
if (window.visualization) {
window.visualization.setPreset('listening');
}
// Simulate voice input (in a real app, this would capture actual audio)
setTimeout(() => {
this.simulateVoiceInput();
}, 1500);
} else {
// Stop "listening" state
this.voiceBtn.classList.remove('listening');
this.voiceBtn.innerHTML = '<i class="fas fa-microphone"></i>';
this.voiceVisualizer.classList.remove('active');
this.chatStatusText.textContent = 'Online';
this.stopVoiceAnimation();
// Update 3D visualization to processing mode
if (window.visualization) {
window.visualization.setPreset('processing');
}
}
}
startVoiceAnimation() {
const bars = this.voiceVisualizer.querySelectorAll('.voice-bar');
bars.forEach(bar => bar.classList.add('listening'));
this.voiceAnimationInterval = setInterval(() => {
bars.forEach(bar => {
const randomHeight = 10 + Math.random() * 30;
bar.style.height = `${randomHeight}px`;
});
}, 100);
}
stopVoiceAnimation() {
if (this.voiceAnimationInterval) {
clearInterval(this.voiceAnimationInterval);
this.voiceAnimationInterval = null;
const bars = this.voiceVisualizer.querySelectorAll('.voice-bar');
bars.forEach(bar => {
bar.classList.remove('listening');
bar.style.height = '10px';
});
}
}
simulateVoiceInput() {
if (!this.isListening) return;
// Simulate capturing voice input
const simulatedQuestions = [
"Can you explain quantum computing?",
"How do neural networks learn?",
"What's the difference between AI and machine learning?",
"Help me understand blockchain technology",
"Explain the concept of derivatives in calculus"
];
const randomQuestion = simulatedQuestions[Math.floor(Math.random() * simulatedQuestions.length)];
// Add user message
this.addMessage(randomQuestion, 'user');
// Process and respond
this.processAIResponse(randomQuestion);
// Stop listening after receiving input
setTimeout(() => {
this.isListening = false;
this.voiceBtn.classList.remove('listening');
this.voiceBtn.innerHTML = '<i class="fas fa-microphone"></i>';
this.voiceVisualizer.classList.remove('active');
this.stopVoiceAnimation();
this.chatStatusText.textContent = 'Online';
}, 500);
}
sendMessage() {
const message = this.chatInput.value.trim();
if (message === '') return;
// Add user message
this.addMessage(message, 'user');
// Clear input
this.chatInput.value = '';
this.autoResizeTextarea();
// Process and respond
this.processAIResponse(message);
}
addMessage(content, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${sender}`;
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
messageDiv.innerHTML = `
<div class="message-content">${this.escapeHtml(content)}</div>
<div class="message-time">${time}</div>
`;
this.chatMessages.appendChild(messageDiv);
// Scroll to bottom
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
// Update 3D visualization
if (window.visualization) {
if (sender === 'user') {
window.visualization.setPreset('processing');
}
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
processAIResponse(userMessage) {
// Show typing indicator
this.showTypingIndicator();
// Update 3D visualization
if (window.visualization) {
window.visualization.setPreset('processing');
}
// Simulate AI processing time
const processingTime = 1000 + Math.random() * 2000;
setTimeout(() => {
this.hideTypingIndicator();
// Generate AI response
const response = this.generateAIResponse(userMessage);
// Add AI response
this.addMessage(response, 'ai');
// Speak the response if voice synthesis is available
if (window.voiceSynthesis) {
setTimeout(() => {
window.voiceSynthesis.speak(response);
}, 300);
}
// Update 3D visualization
if (window.visualization) {
window.visualization.setPreset('responding');
}
// Return to idle after a delay
setTimeout(() => {
if (window.visualization) {
window.visualization.setPreset('idle');
}
}, 3000);
}, processingTime);
}
showTypingIndicator() {
this.typingIndicator.style.display = 'flex';
this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
}
hideTypingIndicator() {
this.typingIndicator.style.display = 'none';
}
generateAIResponse(userMessage) {
// Simple AI response generator
const responses = {
quantum: "Quantum computing leverages quantum mechanics to process information. Unlike classical bits (0 or 1), quantum bits (qubits) can exist in superposition, enabling parallel computation. Key concepts include superposition, entanglement, and quantum interference.",
neural: "Neural networks learn through backpropagation and gradient descent. They adjust weights based on prediction errors, minimizing loss functions. Deep learning uses multiple layers to extract hierarchical features from data.",
blockchain: "Blockchain is a decentralized, distributed ledger technology. Each block contains cryptographic hashes linking to previous blocks, ensuring immutability. Smart contracts enable self-executing agreements on blockchain networks.",
calculus: "Derivatives measure instantaneous rate of change. The derivative of f(x) at point a is the slope of the tangent line. Differentiation rules include power, product, quotient, and chain rules for various functions.",
default: "I understand you're asking about " + userMessage.substring(0, 30) + "... This is a complex topic that requires careful explanation. Could you specify which aspect you'd like me to focus on? I can provide examples, applications, or fundamental principles."
};
const lowerMessage = userMessage.toLowerCase();
if (lowerMessage.includes('quantum')) return responses.quantum;
if (lowerMessage.includes('neural') || lowerMessage.includes('network')) return responses.neural;
if (lowerMessage.includes('blockchain')) return responses.blockchain;
if (lowerMessage.includes('calculus') || lowerMessage.includes('derivative')) return responses.calculus;
return responses.default;
}
}
// Main application
class AITutorVisualization {
constructor() {
if (!checkThreeJS()) return;
this.scene = null;
this.camera = null;
this.renderer = null;
this.particles = null;
this.controls = null;
this.params = {
intensity: 0.5,
particleCount: 2000,
energy: 0.3,
mode: 'idle'
};
this.audioData = new Array(32).fill(0);
this.time = 0;
this.animationFrameId = null;
try {
this.init();
this.createParticles();
this.setupControls();
this.animate();
updateStatus('AI Interface Ready');
setTimeout(hideLoading, 500);
} catch (error) {
console.error('Initialization error:', error);
showError('Failed to initialize 3D scene: ' + error.message);
}
}
init() {
updateStatus('Creating 3D scene...');
// Scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0a0f);
// Camera
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.z = 5;
// Renderer
const canvas = document.getElementById('mainCanvas');
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Add fog
this.scene.fog = new THREE.Fog(0x0a0a0f, 10, 25);
// Add lights
const ambientLight = new THREE.AmbientLight(0x222244, 0.5);
this.scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0x5a6cff, 1);
directionalLight.position.set(5, 3, 5);
this.scene.add(directionalLight);
// Orbit controls
updateStatus('Setting up controls...');
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
}
createParticles() {
updateStatus('Creating particles...');
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(this.params.particleCount * 3);
const colors = new Float32Array(this.params.particleCount * 3);
for (let i = 0; i < this.params.particleCount; i++) {
const i3 = i * 3;
// Fibonacci sphere distribution
const phi = Math.acos(-1 + (2 * i) / this.params.particleCount);
const theta = Math.sqrt(this.params.particleCount * Math.PI) * phi;
const x = Math.cos(theta) * Math.sin(phi);
const y = Math.sin(theta) * Math.sin(phi);
const z = Math.cos(phi);
positions[i3] = x;
positions[i3 + 1] = y;
positions[i3 + 2] = z;
// Color gradient based on position
colors[i3] = 0.5 + x * 0.5; // Red
colors[i3 + 1] = 0.3 + y * 0.7; // Green
colors[i3 + 2] = 0.8 + z * 0.2; // Blue
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.03,
vertexColors: true,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
this.particles = new THREE.Points(geometry, material);
this.scene.add(this.particles);
// Add a test sphere to verify rendering
const testGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const testMaterial = new THREE.MeshBasicMaterial({
color: 0x5a6cff,
wireframe: true,
transparent: true,
opacity: 0.1
});
const testSphere = new THREE.Mesh(testGeometry, testMaterial);
this.scene.add(testSphere);
}
setupControls() {
// Slider controls
const intensitySlider = document.getElementById('intensity');
const particleCountSlider = document.getElementById('particleCount');
const energySlider = document.getElementById('energy');
const intensityValue = document.getElementById('intensityValue');
const particleCountValue = document.getElementById('particleCountValue');
const energyValue = document.getElementById('energyValue');
intensitySlider.addEventListener('input', (e) => {
this.params.intensity = e.target.value / 100;
intensityValue.textContent = e.target.value;
});
particleCountSlider.addEventListener('input', (e) => {
this.params.particleCount = parseInt(e.target.value);
particleCountValue.textContent = e.target.value;
this.updateParticleCount();
});
energySlider.addEventListener('input', (e) => {
this.params.energy = e.target.value / 100;
energyValue.textContent = e.target.value;
});
// Preset buttons
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const preset = e.target.dataset.preset;
this.setPreset(preset);
});
});
// Retry button
document.getElementById('retryBtn').addEventListener('click', () => {
location.reload();
});
}
setPreset(preset) {
this.params.mode = preset;
// Update UI to show active preset
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.style.background = btn.dataset.preset === preset
? 'rgba(90, 108, 255, 0.4)'
: 'rgba(90, 108, 255, 0.1)';
});
// Update status
const statusMap = {
'listening': '🎤 Listening for input...',
'processing': '⚡ Processing thoughts...',
'responding': '💬 Formulating response...',
'exploring': '🔍 Exploring concepts...',
'teaching': '📚 Teaching mode active',
'idle': '🌀 AI Tutor Idle'
};
if (statusMap[preset]) {
updateStatus(statusMap[preset]);
}
}
updateParticleCount() {
if (this.particles) {
this.scene.remove(this.particles);
}
this.createParticles();
}
simulateAudioData() {
// Simulate audio data for visualization
const time = performance.now() * 0.001;
const frequency = 0.5 + this.params.energy * 2;
for (let i = 0; i < this.audioData.length; i++) {
const base = Math.sin(time * frequency + i * 0.3) * 0.5 + 0.5;
const variation = Math.sin(time * 2 + i * 0.5) * 0.2;
this.audioData[i] = base + variation * this.params.intensity;
}
}
updateParticles() {
if (!this.particles) return;
const positions = this.particles.geometry.attributes.position.array;
const originalPositions = this.particles.geometry.attributes.originalPosition;
// Store original positions if not already stored
if (!originalPositions) {
const origPos = new Float32Array(positions.length);
origPos.set(positions);
this.particles.geometry.setAttribute('originalPosition', new THREE.BufferAttribute(origPos, 3));
}
const origPositions = this.particles.geometry.attributes.originalPosition.array;
const time = this.time;
for (let i = 0; i < this.params.particleCount; i++) {
const i3 = i * 3;
const x = origPositions[i3];
const y = origPositions[i3 + 1];
const z = origPositions[i3 + 2];
let radius = 2.0;
let intensity = this.params.intensity;
// Apply different behaviors based on mode
switch(this.params.mode) {
case 'listening':
radius += Math.sin(time * 3 + i * 0.01) * 0.5 * intensity;
break;
case 'processing':
radius += Math.sin(time * 5 + i * 0.02) * 0.6 * intensity;
break;
case 'responding':
radius += (Math.sin(time * 2 + i * 0.005) + 1) * 0.4 * intensity;
break;
case 'exploring':
radius += Math.sin(time * 1.5 + i * 0.015) * 0.7 * intensity;
break;
case 'teaching':
radius += Math.sin(time * 4 + i * 0.008) * 0.55 * intensity;
break;
default: // idle
radius += Math.sin(time * 0.5 + i * 0.01) * 0.2 * intensity;
}
// Add energy-based pulsation
radius += Math.sin(time * 8) * 0.2 * this.params.energy;
// Add audio data influence
const audioIndex = Math.floor((i / this.params.particleCount) * this.audioData.length);
radius += this.audioData[audioIndex] * 0.5 * intensity;
positions[i3] = x * radius;
positions[i3 + 1] = y * radius;
positions[i3 + 2] = z * radius;
}
this.particles.geometry.attributes.position.needsUpdate = true;
// Rotate particles slowly
this.particles.rotation.y = time * 0.1;
this.particles.rotation.x = Math.sin(time * 0.05) * 0.1;
}
animate() {
this.animationFrameId = requestAnimationFrame(() => this.animate());
this.time += 0.016;
this.simulateAudioData();
this.updateParticles();
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
destroy() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
if (this.controls) {
this.controls.dispose();
}
if (this.renderer) {
this.renderer.dispose();
}
}
}
// Initialize when page loads
window.addEventListener('load', () => {
updateStatus('Loading AI Interface...');
// Wait a moment for libraries to load
setTimeout(() => {
// Initialize 3D visualization
window.visualization = new AITutorVisualization();
// Initialize chat interface
window.chatInterface = new ChatInterface();
// Initialize voice synthesis
if ('speechSynthesis' in window) {
window.voiceSynthesis = new VoiceSynthesis();
updateStatus('Voice synthesis initialized');
} else {
console.warn('Speech synthesis not supported in this browser');
document.getElementById('voiceStatus').style.display = 'flex';
document.getElementById('statusMessage').textContent = 'Voice synthesis not supported';
document.getElementById('statusMessage').style.color = '#ff5a5a';
}
if (window.visualization && window.visualization.scene) {
// Handle window resize
window.addEventListener('resize', () => window.visualization.onWindowResize());
// Handle key controls
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
window.visualization.setPreset(
window.visualization.params.mode === 'idle' ? 'listening' : 'idle'
);
}
// Ctrl+Space to stop speech
if (e.code === 'Space' && e.ctrlKey && window.voiceSynthesis) {
window.voiceSynthesis.stopSpeaking();
}
});
// Set initial preset
window.visualization.setPreset('idle');
}
}, 100);
});
// Handle page unload
window.addEventListener('beforeunload', () => {
if (window.visualization) {
window.visualization.destroy();
}
if (window.voiceSynthesis) {
window.voiceSynthesis.stopSpeaking();
}
});
</script>
</body>
</html>