feat(presenter): add browser TTS (Web Speech API) + fix German umlauts
- Integrate Web Speech API into usePresenterMode for text-to-speech - Speech-driven paragraph advancement (falls back to timer if TTS unavailable) - TTS toggle button (Volume2/VolumeX) in PresenterOverlay - Chrome keepAlive workaround for long speeches - Voice selection: prefers premium/neural voices, falls back to any matching lang - Fix all German umlauts across presenter-script, presenter-faq, i18n, route.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { Play, Pause, Square, SkipForward } from 'lucide-react'
|
||||
import { Play, Pause, Square, SkipForward, Volume2, VolumeX } from 'lucide-react'
|
||||
import { Language } from '@/lib/types'
|
||||
import { PresenterState } from '@/lib/presenter/types'
|
||||
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
|
||||
@@ -18,6 +18,10 @@ interface PresenterOverlayProps {
|
||||
onResume: () => void
|
||||
onStop: () => void
|
||||
onSkip: () => void
|
||||
isSpeaking?: boolean
|
||||
ttsAvailable?: boolean
|
||||
ttsEnabled?: boolean
|
||||
onToggleTts?: () => void
|
||||
}
|
||||
|
||||
export default function PresenterOverlay({
|
||||
@@ -31,6 +35,10 @@ export default function PresenterOverlay({
|
||||
onResume,
|
||||
onStop,
|
||||
onSkip,
|
||||
isSpeaking,
|
||||
ttsAvailable,
|
||||
ttsEnabled = true,
|
||||
onToggleTts,
|
||||
}: PresenterOverlayProps) {
|
||||
const i = t(lang)
|
||||
const slideName = i.slideNames[currentIndex] || SLIDE_ORDER[currentIndex] || ''
|
||||
@@ -79,11 +87,32 @@ export default function PresenterOverlay({
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* TTS toggle */}
|
||||
{ttsAvailable && onToggleTts && (
|
||||
<button
|
||||
onClick={onToggleTts}
|
||||
className={`w-7 h-7 rounded-full flex items-center justify-center transition-colors ${
|
||||
ttsEnabled
|
||||
? 'bg-indigo-500/30 hover:bg-indigo-500/50'
|
||||
: 'bg-white/10 hover:bg-white/20'
|
||||
}`}
|
||||
title={lang === 'de'
|
||||
? (ttsEnabled ? 'Stimme ausschalten' : 'Stimme einschalten')
|
||||
: (ttsEnabled ? 'Mute voice' : 'Enable voice')}
|
||||
>
|
||||
{ttsEnabled ? (
|
||||
<Volume2 className={`w-3.5 h-3.5 ${isSpeaking ? 'text-indigo-300' : 'text-white/60'}`} />
|
||||
) : (
|
||||
<VolumeX className="w-3.5 h-3.5 text-white/40" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onSkip}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
|
||||
hover:bg-white/20 transition-colors"
|
||||
title={lang === 'de' ? 'Naechste Folie' : 'Next slide'}
|
||||
title={lang === 'de' ? 'Nächste Folie' : 'Next slide'}
|
||||
>
|
||||
<SkipForward className="w-3.5 h-3.5 text-white/60" />
|
||||
</button>
|
||||
@@ -138,7 +167,7 @@ export default function PresenterOverlay({
|
||||
{/* State message */}
|
||||
{state === 'paused' && (
|
||||
<p className="text-xs text-yellow-400/60 mt-1">
|
||||
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder druecken Sie Play' : 'Paused — ask a question or press play'}
|
||||
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder drücken Sie Play' : 'Paused — ask a question or press play'}
|
||||
</p>
|
||||
)}
|
||||
{state === 'answering' && (
|
||||
@@ -148,7 +177,7 @@ export default function PresenterOverlay({
|
||||
)}
|
||||
{state === 'resuming' && (
|
||||
<p className="text-xs text-indigo-400/60 mt-1">
|
||||
{lang === 'de' ? 'Setze Praesentation fort...' : 'Resuming presentation...'}
|
||||
{lang === 'de' ? 'Setze Präsentation fort...' : 'Resuming presentation...'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user