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:
@@ -324,7 +324,7 @@ export default function ChatFAB({
|
||||
bg-indigo-600 hover:bg-indigo-500 text-white
|
||||
flex items-center justify-center shadow-lg shadow-indigo-600/30
|
||||
transition-colors"
|
||||
aria-label={lang === 'de' ? 'Investor Agent oeffnen' : 'Open Investor Agent'}
|
||||
aria-label={lang === 'de' ? 'Investor Agent öffnen' : 'Open Investor Agent'}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
|
||||
@@ -209,6 +209,10 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
onResume={presenter.resume}
|
||||
onStop={presenter.stop}
|
||||
onSkip={presenter.skipSlide}
|
||||
isSpeaking={presenter.isSpeaking}
|
||||
ttsAvailable={presenter.ttsAvailable}
|
||||
ttsEnabled={presenter.ttsEnabled}
|
||||
onToggleTts={() => presenter.setTtsEnabled(!presenter.ttsEnabled)}
|
||||
/>
|
||||
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,11 +53,11 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
|
||||
transition={{ delay: 0.3, duration: 0.5 }}
|
||||
>
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{isDE ? 'KI-Praesentator' : 'AI Presenter'}</GradientText>
|
||||
<GradientText>{isDE ? 'KI-Präsentator' : 'AI Presenter'}</GradientText>
|
||||
</h1>
|
||||
<p className="text-lg text-white/60 max-w-lg mx-auto mb-8">
|
||||
{isDE
|
||||
? 'Ihr persoenlicher KI-Guide durch das BreakPilot ComplAI Pitch Deck. 15 Minuten, alle Fakten, jederzeit unterbrechbar.'
|
||||
? 'Ihr persönlicher KI-Guide durch das BreakPilot ComplAI Pitch Deck. 15 Minuten, alle Fakten, jederzeit unterbrechbar.'
|
||||
: 'Your personal AI guide through the BreakPilot ComplAI pitch deck. 15 minutes, all facts, interruptible at any time.'}
|
||||
</p>
|
||||
</motion.div>
|
||||
@@ -79,12 +79,12 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
|
||||
{isPresenting ? (
|
||||
<>
|
||||
<Pause className="w-5 h-5" />
|
||||
{isDE ? 'Praesentation laeuft...' : 'Presentation running...'}
|
||||
{isDE ? 'Präsentation läuft...' : 'Presentation running...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-5 h-5" />
|
||||
{isDE ? 'Praesentation starten' : 'Start Presentation'}
|
||||
{isDE ? 'Präsentation starten' : 'Start Presentation'}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -108,7 +108,7 @@ export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenti
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-1.5 py-0.5 bg-white/10 rounded text-xs font-mono">ESC</span>
|
||||
<span>{isDE ? 'Slide-Uebersicht' : 'Slide Overview'}</span>
|
||||
<span>{isDE ? 'Slide-Übersicht' : 'Slide Overview'}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user