|
|
<div class="pron-container"> |
|
|
|
|
|
<div class="header"> |
|
|
<div class="dropdown-row" aria-hidden="false"> |
|
|
<label for="pronModeSelect" class="visually-hidden">Pronunciation Mode</label> |
|
|
<select id="pronModeSelect" class="select-dropdown" aria-label="Pronunciation Mode" (change)="onModeChange($any($event.target).value)"> |
|
|
<option value="phonetics">Phoneme-Level Accuracy Check</option> |
|
|
<option value="waveform">Audio Similarity Check</option> |
|
|
<option value="normaltext" disabled>Speech-to-Text Accuracy Check</option> |
|
|
<option value="prosody" disabled>Prosody Check</option> |
|
|
<option value="embedding" disabled>Embedding Similarity Check</option> |
|
|
</select> |
|
|
</div> |
|
|
<h2 class="title">Pronunciation Trainer</h2> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="main-content"> |
|
|
|
|
|
<div class="image-section"> |
|
|
<img [src]="imgsrc" [alt]="word" class="apple-image"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="controls-section"> |
|
|
|
|
|
<div class="word-section"> |
|
|
<h2 class="word">{{ word }}</h2> |
|
|
<p class="phonetics">{{ phonetics }}</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="teacher-section"> |
|
|
<img src="assets/images/chat/natasha.png" alt="Teacher" class="avatar"> |
|
|
<button class="btn btn-primary" |
|
|
id="playTeacherBtn" |
|
|
(click)="playTeacherAudio()" |
|
|
[disabled]="isTeacherLoading" |
|
|
[attr.aria-busy]="isTeacherLoading"> |
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<polygon points="5 3 19 12 5 21 5 3"></polygon> |
|
|
</svg> |
|
|
<span class="btn-text" *ngIf="!isTeacherLoading">Play Teacher</span> |
|
|
<span class="btn-text" *ngIf="isTeacherLoading">Loading...</span> |
|
|
<svg class="spinner" viewBox="0 0 24 24" [class.hidden]="!isTeacherLoading"> |
|
|
<circle class="spinner-circle" cx="12" cy="12" r="10"></circle> |
|
|
</svg> |
|
|
</button> |
|
|
|
|
|
<div class="voice-selection-container"> |
|
|
<div class="voice-toggle-control"> |
|
|
<div class="voice-label-group toggle-label-left" |
|
|
[class.active]="!isOriginal" |
|
|
(click)="isOriginal = false; updateSelection()"> |
|
|
<span class="voice-text">Original Voice</span> |
|
|
</div> |
|
|
|
|
|
<div class="voice-label-group toggle-label-right" |
|
|
[class.active]="isOriginal" |
|
|
(click)="isOriginal = true; updateSelection()"> |
|
|
<span class="voice-text">Cloned Voice</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="student-section"> |
|
|
<img src="assets/images/pron/student.png" alt="Student" class="avatar"> |
|
|
<div class="button-group"> |
|
|
<button class="btn btn-secondary record-btn" |
|
|
id="recordBtn" |
|
|
(click)="isRecording ? stopRecording() : startRecording()" |
|
|
[class.recording]="isRecording" |
|
|
[attr.aria-pressed]="isRecording"> |
|
|
<ng-container *ngIf="!isRecording; else stopTpl"> |
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"> |
|
|
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path> |
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> |
|
|
<line x1="12" x2="12" y1="19" y2="22"></line> |
|
|
</svg> |
|
|
<span class="btn-text">Start Recording</span> |
|
|
</ng-container> |
|
|
|
|
|
<ng-template #stopTpl> |
|
|
<svg class="icon stop-icon" viewBox="0 0 24 24" fill="currentColor" stroke="none" width="18" height="18" aria-hidden="true"> |
|
|
<rect x="6" y="6" width="12" height="12" rx="2"></rect> |
|
|
</svg> |
|
|
<span class="btn-text">Stop</span> |
|
|
</ng-template> |
|
|
</button> |
|
|
|
|
|
|
|
|
<button class="btn btn-accent" |
|
|
id="playStudentBtn" |
|
|
*ngIf="recordedBlobUrl" |
|
|
(click)="playRecorded()"> |
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<polygon points="5 3 19 12 5 21 5 3"></polygon> |
|
|
</svg> |
|
|
<span class="btn-text">Play My Recording</span> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div *ngIf="pronMode === 'waveform'" class="waveform-row"> |
|
|
<div class="waveform-block"> |
|
|
<div class="waveform-title">Teacher</div> |
|
|
<div id="teacherWaveform" class="waveform-canvas"></div> |
|
|
</div> |
|
|
|
|
|
<div class="waveform-block"> |
|
|
<div class="waveform-title">Student</div> |
|
|
<div id="studentWaveform" class="waveform-canvas"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<button class="btn btn-success full-width" |
|
|
id="checkBtn" |
|
|
(click)="checkPronunciation()" |
|
|
[disabled]="isChecking || !recordedBlobUrl" |
|
|
[attr.aria-busy]="isChecking" |
|
|
[attr.aria-disabled]="isChecking || !recordedBlobUrl" |
|
|
aria-label="Check pronunciation"> |
|
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" *ngIf="!isChecking"> |
|
|
<polyline points="20 6 9 17 4 12"></polyline> |
|
|
</svg> |
|
|
|
|
|
<span class="btn-text" *ngIf="!isChecking">Check Pronunciation</span> |
|
|
<span class="btn-text" *ngIf="isChecking">Checking...</span> |
|
|
|
|
|
<svg class="spinner" viewBox="0 0 24 24" *ngIf="isChecking"> |
|
|
<circle class="spinner-circle" cx="12" cy="12" r="10"></circle> |
|
|
</svg> |
|
|
</button> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="results-section" id="resultsSection"> |
|
|
|
|
|
|
|
|
<div class="gauge-wrapper"> |
|
|
<div class="gauge"> |
|
|
<div class="gauge-arc"></div> |
|
|
|
|
|
<div class="needle" [style.--angle]="needleAngle + 'deg'" aria-hidden="true"></div> |
|
|
</div> |
|
|
|
|
|
<div class="mic-badge"> |
|
|
<span class="score-span" aria-live="polite">{{ result?.score ?? 0 }}%</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="suggestions-section" *ngIf="suggestions.length > 0"> |
|
|
<h3 class="suggestions-title">Feedback & Suggestions</h3> |
|
|
|
|
|
|
|
|
<div class="sugg-div"> |
|
|
<div class="suggestions-page"> |
|
|
<div class="suggestion-card" *ngFor="let s of pagedSuggestions; let i = index"> |
|
|
<div class="suggestion-badge" aria-hidden="true">{{ s.id ?? (suggestionPage * suggestionsPerPage + i + 1) }}</div> |
|
|
<div class="suggestion-body"> |
|
|
<div class="suggestion-point">{{ s.title }}</div> |
|
|
<div class="suggestion-feedback" [innerHTML]="s.message"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="suggestion-nav"> |
|
|
<button class="btn btn-outline btn-nav pagebtn" (click)="prevSuggestion()" [disabled]="suggestionPage === 0" aria-label="Previous feedback">◀</button> |
|
|
|
|
|
<div class="nav-info"> |
|
|
{{ suggestionPage + 1 }} / {{ totalSuggestionPages }} |
|
|
</div> |
|
|
|
|
|
<button class="btn btn-outline btn-nav pagebtn" (click)="nextSuggestion()" [disabled]="suggestionPage >= totalSuggestionPages - 1" aria-label="Next feedback">▶</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="suggestions-section" *ngIf="pronMode !== 'select' && (!suggestions || suggestions.length === 0) && result?.suggestion"> |
|
|
<h3 class="suggestions-title">Feedback & Suggestions</h3> |
|
|
<div class="suggestion-item"> |
|
|
<div class="suggestion-content"> |
|
|
<p class="suggestion-feedback">{{ result?.suggestion }}</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div> |
|
|
<div class="nav-buttons"> |
|
|
<button class="btn btn-outline btn-nav" (click)="prevQuestion()" [disabled]="currentIndex === 0" aria-label="Previous"> |
|
|
◀ Prev |
|
|
</button> |
|
|
|
|
|
<div class="nav-info"> |
|
|
{{ currentIndex + 1 }} / {{ questions.length }} |
|
|
</div> |
|
|
|
|
|
<button class="btn btn-outline btn-nav" (click)="nextQuestion()" [disabled]="currentIndex === questions.length - 1" aria-label="Next"> |
|
|
Next ▶ |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button aria-label="Close" class="user-guide-close-icon" (click)="closePopup()">×</button> |
|
|
|
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|