muboboev's picture
Подэтап 5.3 — GameQuest
be3690e verified
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';
// Start timer
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 {
// Simulate API response for demo
await new Promise(resolve => setTimeout(resolve, 1500));
// Check if we're in a game context
const isGamePage = window.location.pathname.includes('gamequest');
if (isGamePage) {
// Game-specific analysis with higher scores
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
};
// Update game score if on game page
if (typeof updateScore === 'function') {
const points = Math.floor(this.analysisResult.confidence / 10);
updateScore(points);
}
} else {
// Regular analysis
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);