| | 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); |