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)