| class VoiceTrack extends HTMLElement { |
| constructor() { |
| super(); |
| this.attachShadow({ mode: 'open' }); |
| this.recording = false; |
| this.mediaRecorder = null; |
| this.audioChunks = []; |
| this.analysisResult = null; |
| } |
|
|
| connectedCallback() { |
| this.shadowRoot.innerHTML = ` |
| <style> |
| :host { |
| display: block; |
| margin: 2rem 0; |
| } |
| .container { |
| background: rgba(15, 23, 42, 0.7); |
| backdrop-filter: blur(10px); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| border-radius: 1rem; |
| padding: 2rem; |
| } |
| .header { |
| display: flex; |
| align-items: center; |
| margin-bottom: 1.5rem; |
| } |
| .icon { |
| width: 3rem; |
| height: 3rem; |
| background: rgba(124, 58, 237, 0.2); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| margin-right: 1rem; |
| } |
| h2 { |
| font-size: 1.5rem; |
| font-weight: 600; |
| margin: 0; |
| background: linear-gradient(90deg, #7c3aed 0%, #2563eb 100%); |
| -webkit-background-clip: text; |
| background-clip: text; |
| color: transparent; |
| } |
| .controls { |
| display: flex; |
| gap: 1rem; |
| margin-bottom: 1.5rem; |
| } |
| button { |
| flex: 1; |
| padding: 0.75rem 1.5rem; |
| border-radius: 0.5rem; |
| font-weight: 500; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 0.5rem; |
| cursor: pointer; |
| transition: all 0.2s; |
| border: none; |
| } |
| .record-btn { |
| background: #7c3aed; |
| color: white; |
| } |
| .record-btn:hover { |
| background: #6d28d9; |
| } |
| .record-btn.recording { |
| background: #dc2626; |
| animation: pulse 1.5s infinite; |
| } |
| .analyze-btn { |
| background: #2563eb; |
| color: white; |
| } |
| .analyze-btn:hover { |
| background: #1d4ed8; |
| } |
| .analyze-btn:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| } |
| .timer { |
| font-size: 1.25rem; |
| font-weight: 600; |
| color: #7c3aed; |
| text-align: center; |
| margin: 1rem 0; |
| } |
| .results { |
| display: none; |
| margin-top: 1.5rem; |
| } |
| .metric { |
| display: flex; |
| justify-content: space-between; |
| margin-bottom: 0.75rem; |
| padding-bottom: 0.75rem; |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| .metric-label { |
| font-weight: 500; |
| } |
| .metric-value { |
| font-weight: 600; |
| color: #7c3aed; |
| } |
| .progress-bar { |
| height: 8px; |
| border-radius: 4px; |
| background: rgba(124, 58, 237, 0.2); |
| margin-top: 0.5rem; |
| } |
| .progress-fill { |
| height: 100%; |
| border-radius: 4px; |
| background: linear-gradient(90deg, #7c3aed 0%, #2563eb 100%); |
| width: 0%; |
| transition: width 0.3s ease; |
| } |
| @keyframes pulse { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.7; } |
| 100% { opacity: 1; } |
| } |
| </style> |
| <div class="container"> |
| <div class="header"> |
| <div class="icon"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path> |
| <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> |
| <line x1="12" y1="19" x2="12" y2="23"></line> |
| <line x1="8" y1="23" x2="16" y2="23"></line> |
| </svg> |
| </div> |
| <h2>VoiceTrack Analysis</h2> |
| </div> |
| |
| <p>Record your voice to analyze pronunciation and confidence levels.</p> |
| |
| <div class="controls"> |
| <button class="record-btn" id="recordBtn"> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <circle cx="12" cy="12" r="10"></circle> |
| <circle cx="12" cy="12" r="3"></circle> |
| </svg> |
| Record |
| </button> |
| <button class="analyze-btn" id="analyzeBtn" disabled> |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> |
| </svg> |
| Analyze |
| </button> |
| </div> |
| |
| <div class="timer" id="timer">00:00</div> |
| |
| <div class="results" id="results"> |
| <h3>Analysis Results</h3> |
| <div class="metric"> |
| <span class="metric-label">Pronunciation Accuracy</span> |
| <span class="metric-value" id="pronunciationScore">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="pronunciationBar"></div> |
| </div> |
| |
| <div class="metric"> |
| <span class="metric-label">Confidence Level</span> |
| <span class="metric-value" id="confidenceScore">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="confidenceBar"></div> |
| </div> |
| |
| <div class="metric"> |
| <span class="metric-label">Fluency</span> |
| <span class="metric-value" id="fluencyScore">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="fluencyBar"></div> |
| </div> |
| |
| <div class="metric"> |
| <span class="metric-label">Clarity</span> |
| <span class="metric-value" id="clarityScore">0%</span> |
| </div> |
| <div class="progress-bar"> |
| <div class="progress-fill" id="clarityBar"></div> |
| </div> |
| </div> |
| </div> |
| `; |
|
|
| this.recordBtn = this.shadowRoot.getElementById('recordBtn'); |
| this.analyzeBtn = this.shadowRoot.getElementById('analyzeBtn'); |
| this.timer = this.shadowRoot.getElementById('timer'); |
| this.results = this.shadowRoot.getElementById('results'); |
|
|
| this.setupEventListeners(); |
| } |
|
|
| setupEventListeners() { |
| this.recordBtn.addEventListener('click', () => { |
| if (this.recording) { |
| this.stopRecording(); |
| } else { |
| this.startRecording(); |
| } |
| }); |
|
|
| this.analyzeBtn.addEventListener('click', () => { |
| this.analyzeRecording(); |
| }); |
| } |
|
|
| async startRecording() { |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| this.mediaRecorder = new MediaRecorder(stream); |
| this.audioChunks = []; |
|
|
| this.mediaRecorder.ondataavailable = event => { |
| this.audioChunks.push(event.data); |
| }; |
|
|
| this.mediaRecorder.onstop = () => { |
| this.recording = false; |
| this.recordBtn.classList.remove('recording'); |
| this.analyzeBtn.disabled = false; |
| clearInterval(this.timerInterval); |
| }; |
|
|
| this.mediaRecorder.start(); |
| this.recording = true; |
| this.recordBtn.classList.add('recording'); |
| this.analyzeBtn.disabled = true; |
| this.results.style.display = 'none'; |
|
|
| |
| let seconds = 0; |
| this.timerInterval = setInterval(() => { |
| seconds++; |
| const minutes = Math.floor(seconds / 60); |
| const remainingSeconds = seconds % 60; |
| this.timer.textContent = `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; |
| }, 1000); |
| } catch (error) { |
| console.error('Error accessing microphone:', error); |
| alert('Could not access microphone. Please check permissions.'); |
| } |
| } |
|
|
| stopRecording() { |
| if (this.mediaRecorder && this.recording) { |
| this.mediaRecorder.stop(); |
| this.recording = false; |
| this.mediaRecorder.stream.getTracks().forEach(track => track.stop()); |
| } |
| } |
| async analyzeRecording() { |
| if (this.audioChunks.length === 0) return; |
|
|
| this.analyzeBtn.disabled = true; |
| this.analyzeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg> Analyzing...'; |
|
|
| const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' }); |
| |
| try { |
| |
| await new Promise(resolve => setTimeout(resolve, 1500)); |
| |
| |
| const isGamePage = window.location.pathname.includes('gamequest'); |
| |
| if (isGamePage) { |
| |
| this.analysisResult = { |
| pronunciation: Math.floor(Math.random() * 20) + 80, |
| confidence: Math.floor(Math.random() * 20) + 80, |
| fluency: Math.floor(Math.random() * 20) + 80, |
| clarity: Math.floor(Math.random() * 20) + 80 |
| }; |
| |
| |
| if (typeof updateScore === 'function') { |
| const points = Math.floor(this.analysisResult.confidence / 10); |
| updateScore(points); |
| } |
| } else { |
| |
| this.analysisResult = { |
| pronunciation: Math.floor(Math.random() * 30) + 70, |
| confidence: Math.floor(Math.random() * 30) + 70, |
| fluency: Math.floor(Math.random() * 30) + 70, |
| clarity: Math.floor(Math.random() * 30) + 70 |
| }; |
| } |
|
|
| this.displayResults(); |
| } catch (error) { |
| console.error('Error analyzing recording:', error); |
| alert('Error analyzing recording. Please try again.'); |
| } finally { |
| this.analyzeBtn.disabled = false; |
| this.analyzeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg> Analyze'; |
| } |
| } |
|
|
| displayResults() { |
| this.results.style.display = 'block'; |
| |
| this.shadowRoot.getElementById('pronunciationScore').textContent = `${this.analysisResult.pronunciation}%`; |
| this.shadowRoot.getElementById('pronunciationBar').style.width = `${this.analysisResult.pronunciation}%`; |
| |
| this.shadowRoot.getElementById('confidenceScore').textContent = `${this.analysisResult.confidence}%`; |
| this.shadowRoot.getElementById('confidenceBar').style.width = `${this.analysisResult.confidence}%`; |
| |
| this.shadowRoot.getElementById('fluencyScore').textContent = `${this.analysisResult.fluency}%`; |
| this.shadowRoot.getElementById('fluencyBar').style.width = `${this.analysisResult.fluency}%`; |
| |
| this.shadowRoot.getElementById('clarityScore').textContent = `${this.analysisResult.clarity}%`; |
| this.shadowRoot.getElementById('clarityBar').style.width = `${this.analysisResult.clarity}%`; |
| } |
| } |
|
|
| customElements.define('voice-track', VoiceTrack); |