feat(pitch-deck): add AI Presenter mode with LiteLLM migration and FAQ system
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

- 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>
This commit is contained in:
Benjamin Admin
2026-03-20 11:45:55 +01:00
parent df0a9d6cf0
commit 3a2567b44d
20 changed files with 2434 additions and 164 deletions

View File

@@ -5,6 +5,7 @@ import { AnimatePresence } from 'framer-motion'
import { useSlideNavigation } from '@/lib/hooks/useSlideNavigation'
import { useKeyboard } from '@/lib/hooks/useKeyboard'
import { usePitchData } from '@/lib/hooks/usePitchData'
import { usePresenterMode } from '@/lib/hooks/usePresenterMode'
import { Language, PitchData } from '@/lib/types'
import ParticleBackground from './ParticleBackground'
@@ -14,7 +15,10 @@ import NavigationFAB from './NavigationFAB'
import ChatFAB from './ChatFAB'
import SlideOverview from './SlideOverview'
import SlideContainer from './SlideContainer'
import PresenterOverlay from './presenter/PresenterOverlay'
import AvatarPlaceholder from './presenter/AvatarPlaceholder'
import IntroPresenterSlide from './slides/IntroPresenterSlide'
import CoverSlide from './slides/CoverSlide'
import ProblemSlide from './slides/ProblemSlide'
import SolutionSlide from './slides/SolutionSlide'
@@ -45,6 +49,13 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
const nav = useSlideNavigation()
const [fabOpen, setFabOpen] = useState(false)
const presenter = usePresenterMode({
goToSlide: nav.goToSlide,
currentSlide: nav.currentIndex,
totalSlides: nav.totalSlides,
language: lang,
})
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
@@ -66,6 +77,7 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
onFullscreen: toggleFullscreen,
onLanguageToggle: onToggleLanguage,
onMenuToggle: toggleMenu,
onPresenterToggle: presenter.toggle,
onGoToSlide: nav.goToSlide,
enabled: !nav.showOverview,
})
@@ -96,6 +108,14 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
if (!data) return null
switch (nav.currentSlide) {
case 'intro-presenter':
return (
<IntroPresenterSlide
lang={lang}
onStartPresenter={presenter.start}
isPresenting={presenter.state !== 'idle'}
/>
)
case 'cover':
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
case 'problem':
@@ -163,6 +183,8 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
currentIndex={nav.currentIndex}
visitedSlides={nav.visitedSlides}
onGoToSlide={nav.goToSlide}
presenterState={presenter.state}
onPresenterInterrupt={presenter.pause}
/>
<NavigationFAB
@@ -174,6 +196,21 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
onToggleLanguage={onToggleLanguage}
/>
{/* Presenter UI */}
<AvatarPlaceholder state={presenter.state} />
<PresenterOverlay
state={presenter.state}
currentIndex={nav.currentIndex}
totalSlides={nav.totalSlides}
progress={presenter.progress}
displayText={presenter.displayText}
lang={lang}
onPause={presenter.pause}
onResume={presenter.resume}
onStop={presenter.stop}
onSkip={presenter.skipSlide}
/>
<AnimatePresence>
{nav.showOverview && (
<SlideOverview