Address classroom tester feedback (Detlef / FSC e.V.)

Fixes the most impactful issues from ~1.5h of testing:

- TTS: spell SID prefixes phonetically (ANEKI → Alfa November Echo Kilo
  India) so unfamiliar waypoint names are intelligible without prior
  briefing context.
- TTS: expand standalone uppercase waypoints after via/direct (with a
  skip list for common ATC English tokens like MAYDAY, CLEARED, …).
- TTS: join taxi route tokens with ", " so pauses land between
  taxiways (C5, Z5, U10, …) instead of running together.
- TTS: handle "ILS Z 25C" variant before the runway → "ILS Zulu runway
  two five center" (was previously read as "Zee twentyfive cee").
- Scenarios: derive arrivalRunway from the chosen approach so the
  controller no longer clears a flight for ILS 25C onto runway 18.
- Radio check: accept any readability 1–5 (numeric or spoken), shorten
  placeholder so it fits the sm-width field.
- Line-up readback: clearer hint about the runway-first ICAO order.
- Classroom UI: disable browser autocomplete/autocorrect on readback
  inputs (Edge autofill was injecting unrelated values).
- Classroom UI: "Speak answer" button replays the expected readback as
  TTS so students can hear the correct phrasing.

Tests adjusted for the new SID and taxi-route phonetics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
itsrubberduck
2026-05-26 10:24:48 +02:00
parent 1f6c068f92
commit 9082a25cb8
6 changed files with 87 additions and 23 deletions

View File

@@ -1077,6 +1077,10 @@
:placeholder="fieldPlaceholder(segment.key)"
:inputmode="fieldInputmode(segment.key)"
:ref="el => assignReadbackFieldRef(segment.key, el as HTMLInputElement | null)"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck="false"
/>
<v-icon v-if="fieldPass(segment.key)" size="16" class="blank-status ok">mdi-check</v-icon>
<v-icon v-else-if="fieldHasAnswer(segment.key)" size="16" class="blank-status warn">mdi-alert
@@ -1101,6 +1105,17 @@
<v-icon size="18">mdi-auto-fix</v-icon>
Auto-fill
</button>
<button
v-if="correctReadbackText"
class="btn ghost"
type="button"
:disabled="ttsLoading"
@click="speakCorrectReadback"
title="Speak correct readback"
>
<v-icon size="18">mdi-volume-high</v-icon>
Speak answer
</button>
</div>
<div v-if="result" class="score">
<div class="score-num">{{ result.score }}%</div>
@@ -3351,6 +3366,22 @@ const targetPhrase = computed(() => {
if (!activeLesson.value || !scenario.value) return ''
return displayCallsign(activeLesson.value.phrase(scenario.value), scenario.value)
})
const correctReadbackText = computed(() => {
if (!activeLesson.value || !scenario.value) return ''
return activeLesson.value.readback.map(seg => {
if (seg.type === 'text') return typeof seg.text === 'function' ? seg.text(scenario.value!) : seg.text
const field = activeLesson.value!.fields.find(f => f.key === seg.key)
return field ? field.expected(scenario.value!) : ''
}).join('').trim()
})
async function speakCorrectReadback() {
const text = correctReadbackText.value
if (!text || ttsLoading.value) return
await say(text)
}
const lessonInfo = computed(() => (activeLesson.value && scenario.value ? activeLesson.value.info(scenario.value) : []))
const lessonReference = computed(() => {