diff --git a/app/pages/classroom.vue b/app/pages/classroom.vue
index 2ad93c6..38f12be 100644
--- a/app/pages/classroom.vue
+++ b/app/pages/classroom.vue
@@ -1017,6 +1017,7 @@
:aria-label="fieldLabel(segment.key)"
:placeholder="fieldPlaceholder(segment.key)"
:inputmode="fieldInputmode(segment.key)"
+ :ref="el => assignReadbackFieldRef(segment.key, el as HTMLInputElement | null)"
/>
mdi-check
mdi-alert
@@ -2590,6 +2591,7 @@ const hasSpokenTarget = ref(false)
const pendingAutoSay = ref(false)
const activeFrequency = ref(null)
const userAnswers = reactive>({})
+const readbackFieldRefs = new Map()
const result = ref(null)
const evaluating = ref(false)
const ttsLoading = ref(false)
@@ -3019,6 +3021,39 @@ const fieldMap = computed>(() => {
return map
})
+const firstReadbackFieldKey = computed(() => {
+ const lesson = activeLesson.value
+ if (!lesson) return null
+ const firstField = lesson.readback.find(segment => segment.type === 'field')
+ if (!firstField || firstField.type !== 'field') return null
+ return firstField.key
+})
+
+function assignReadbackFieldRef(key: string, el: HTMLInputElement | null) {
+ if (!el) {
+ readbackFieldRefs.delete(key)
+ return
+ }
+ readbackFieldRefs.set(key, el)
+}
+
+function focusFirstReadbackField() {
+ const key = firstReadbackFieldKey.value
+ if (!key) return
+ const target = readbackFieldRefs.get(key)
+ if (target) {
+ target.focus()
+ target.select?.()
+ return
+ }
+ void nextTick(() => {
+ const fallback = readbackFieldRefs.get(key)
+ if (!fallback) return
+ fallback.focus()
+ fallback.select?.()
+ })
+}
+
const fieldStates = computed>(() => {
const map: Record = {}
if (!activeLesson.value || !scenario.value) return map
@@ -3308,6 +3343,9 @@ async function speakTarget(auto = false) {
pendingAutoSay.value = false
}
hasSpokenTarget.value = true
+ if (auto) {
+ focusFirstReadbackField()
+ }
await say(phrase)
}
@@ -3991,14 +4029,17 @@ async function playAudioSource(source: CachedAudio, targetRate: number) {
const nativeRate = supportsNativeSpeed
? clampRate(typeof source.speed === 'number' && Number.isFinite(source.speed) ? source.speed : desiredRate, 0.5, 2)
: 1
- const playbackRate = supportsNativeSpeed
+ const basePlaybackRate = supportsNativeSpeed
? clampRate(desiredRate / (nativeRate || 1), 0.5, 2)
: desiredRate
+ const needsRateFallback = !supportsNativeSpeed && Math.abs(basePlaybackRate - 1) > 0.0001
+ const playbackRate = needsRateFallback ? 1 : basePlaybackRate
+ const htmlPlaybackRate = needsRateFallback ? basePlaybackRate : playbackRate
const playWithoutEffects = async () => {
const audio = new Audio(dataUrl)
audio.preload = 'auto'
- audio.playbackRate = playbackRate
+ audio.playbackRate = htmlPlaybackRate
setMediaElementPreservesPitch(audio, true)
audioElement.value = audio
audio.onended = () => {
@@ -4025,11 +4066,6 @@ async function playAudioSource(source: CachedAudio, targetRate: number) {
throw new Error('Audio engine unavailable')
}
- if (!supportsNativeSpeed && Math.abs(playbackRate - 1) > 0.0001) {
- await playWithoutEffects()
- return
- }
-
const sound = await pizzicato.createSoundFromBase64(ctx, source.base64)
sound.setPlaybackRate(playbackRate)
const profile = getReadabilityProfile(readability)