Files
breakpilot-core/pitch-deck/components/presenter/PresenterOverlay.tsx
Benjamin Admin 3a2567b44d
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 27s
CI / test-python-voice (push) Successful in 25s
CI / test-bqas (push) Successful in 25s
CI / Deploy (push) Successful in 4s
feat(pitch-deck): add AI Presenter mode with LiteLLM migration and FAQ system
- Migrate chat API from Ollama to LiteLLM (OpenAI-compatible SSE)
- Add 15-min presenter storyline with bilingual scripts for all 20 slides
- Add FAQ system (30 entries) with keyword matching for instant answers
- Add IntroPresenterSlide with avatar placeholder and start button
- Add PresenterOverlay (progress bar, subtitle text, play/pause/stop)
- Add AvatarPlaceholder with pulse animation during speaking
- Add usePresenterMode hook (state machine: idle→presenting→paused→answering→resuming)
- Add 'P' keyboard shortcut to toggle presenter mode
- Support [GOTO:slide-id] markers in chat responses
- Dynamic slide count (was hardcoded 13, now from SLIDE_ORDER)
- TTS stub prepared for future Piper integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:45:55 +01:00

162 lines
6.2 KiB
TypeScript

'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { Play, Pause, Square, SkipForward } 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
}
export default function PresenterOverlay({
state,
currentIndex,
totalSlides,
progress,
displayText,
lang,
onPause,
onResume,
onStop,
onSkip,
}: 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">
<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'}
>
<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 druecken 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 Praesentation fort...' : 'Resuming presentation...'}
</p>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
)
}