Files
breakpilot-core/pitch-deck/components/presenter/PresenterOverlay.tsx
Benjamin Admin bcbceba31c 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>
2026-03-20 12:11:12 +01:00

191 lines
7.5 KiB
TypeScript

'use client'
import { motion, AnimatePresence } from 'framer-motion'
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'
import { t } from '@/lib/i18n'
interface PresenterOverlayProps {
state: PresenterState
currentIndex: number
totalSlides: number
progress: number
displayText: string
lang: Language
onPause: () => void
onResume: () => void
onStop: () => void
onSkip: () => void
isSpeaking?: boolean
ttsAvailable?: boolean
ttsEnabled?: boolean
onToggleTts?: () => void
}
export default function PresenterOverlay({
state,
currentIndex,
totalSlides,
progress,
displayText,
lang,
onPause,
onResume,
onStop,
onSkip,
isSpeaking,
ttsAvailable,
ttsEnabled = true,
onToggleTts,
}: PresenterOverlayProps) {
const i = t(lang)
const slideName = i.slideNames[currentIndex] || SLIDE_ORDER[currentIndex] || ''
return (
<AnimatePresence>
{state !== 'idle' && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeOut' }}
className="fixed bottom-0 left-0 right-0 z-40 pointer-events-none"
>
<div className="mx-auto max-w-4xl px-4 pb-4 pointer-events-auto">
<div className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl">
{/* Progress bar */}
<div className="h-1 bg-white/5">
<motion.div
className="h-full bg-gradient-to-r from-indigo-500 to-purple-500"
style={{ width: `${Math.min(progress, 100)}%` }}
transition={{ duration: 0.3 }}
/>
</div>
{/* Content */}
<div className="px-4 py-3">
{/* Top row: slide info + controls */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
{/* State indicator */}
<div className={`w-2 h-2 rounded-full ${
state === 'presenting' ? 'bg-green-400 animate-pulse' :
state === 'paused' ? 'bg-yellow-400' :
state === 'answering' ? 'bg-blue-400 animate-pulse' :
state === 'resuming' ? 'bg-indigo-400 animate-pulse' :
'bg-white/30'
}`} />
<span className="text-xs text-white/50 font-medium">
{lang === 'de' ? 'Folie' : 'Slide'} {currentIndex + 1}/{totalSlides} {slideName}
</span>
<span className="text-xs text-white/30">
{Math.round(progress)}%
</span>
</div>
{/* 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' ? 'Nächste Folie' : 'Next slide'}
>
<SkipForward className="w-3.5 h-3.5 text-white/60" />
</button>
{state === 'presenting' ? (
<button
onClick={onPause}
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center
hover:bg-white/20 transition-colors"
title={lang === 'de' ? 'Pausieren' : 'Pause'}
>
<Pause className="w-3.5 h-3.5 text-white/60" />
</button>
) : (
<button
onClick={onResume}
className="w-7 h-7 rounded-full bg-indigo-500/30 flex items-center justify-center
hover:bg-indigo-500/50 transition-colors"
title={lang === 'de' ? 'Fortsetzen' : 'Resume'}
>
<Play className="w-3.5 h-3.5 text-indigo-300" />
</button>
)}
<button
onClick={onStop}
className="w-7 h-7 rounded-full bg-red-500/20 flex items-center justify-center
hover:bg-red-500/30 transition-colors"
title={lang === 'de' ? 'Stoppen' : 'Stop'}
>
<Square className="w-3 h-3 text-red-400" />
</button>
</div>
</div>
{/* Subtitle text */}
<AnimatePresence mode="wait">
{displayText && (
<motion.p
key={displayText.slice(0, 30)}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -5 }}
transition={{ duration: 0.3 }}
className="text-sm text-white/70 leading-relaxed"
>
{displayText}
</motion.p>
)}
</AnimatePresence>
{/* State message */}
{state === 'paused' && (
<p className="text-xs text-yellow-400/60 mt-1">
{lang === 'de' ? 'Pausiert — stellen Sie eine Frage oder drücken Sie Play' : 'Paused — ask a question or press play'}
</p>
)}
{state === 'answering' && (
<p className="text-xs text-blue-400/60 mt-1">
{lang === 'de' ? 'Beantworte Ihre Frage...' : 'Answering your question...'}
</p>
)}
{state === 'resuming' && (
<p className="text-xs text-indigo-400/60 mt-1">
{lang === 'de' ? 'Setze Präsentation fort...' : 'Resuming presentation...'}
</p>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}