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