Files
breakpilot-core/pitch-deck/components/PitchDeck.tsx
T
Sharang Parnerkar be126a7a39
Build pitch-deck / build-push-deploy (push) Successful in 1m22s
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 31s
CI / test-python-voice (push) Successful in 30s
CI / test-bqas (push) Successful in 31s
fix(pitch): showcase sidebar shows only filtered slides + AI presenter via FAB
NavigationFAB and SlideOverview now accept slideNames prop and render only the
active slide list (filtered for showcase mode). Adds AI presenter start button
to the FAB footer so it's accessible even when intro-presenter slide is hidden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 22:50:33 +02:00

329 lines
12 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import { AnimatePresence } from 'framer-motion'
import { useSlideNavigation, SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
import { SHOWCASE_HIDDEN_SLIDES } from '@/lib/slide-order'
import { useKeyboard } from '@/lib/hooks/useKeyboard'
import { usePitchData } from '@/lib/hooks/usePitchData'
import { usePresenterMode } from '@/lib/hooks/usePresenterMode'
import { useAuditTracker } from '@/lib/hooks/useAuditTracker'
import { Language, PitchData } from '@/lib/types'
import { t } from '@/lib/i18n'
import { Investor } from '@/lib/hooks/useAuth'
import Link from 'next/link'
import { FolderOpen } from 'lucide-react'
import ParticleBackground from './ParticleBackground'
import ProgressBar from './ProgressBar'
import NavigationControls from './NavigationControls'
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 Watermark from './Watermark'
import IntroPresenterSlide from './slides/IntroPresenterSlide'
import CoverSlide from './slides/CoverSlide'
import ProblemSlide from './slides/ProblemSlide'
import SolutionSlide from './slides/SolutionSlide'
import ProductSlide from './slides/ProductSlide'
import HowItWorksSlide from './slides/HowItWorksSlide'
import MarketSlide from './slides/MarketSlide'
import BusinessModelSlide from './slides/BusinessModelSlide'
import CompetitionSlide from './slides/CompetitionSlide'
import TeamSlide from './slides/TeamSlide'
import FinancialsSlide from './slides/FinancialsSlide'
import TheAskSlide from './slides/TheAskSlide'
import AIQASlide from './slides/AIQASlide'
import AssumptionsSlide from './slides/AssumptionsSlide'
import ArchitectureSlide from './slides/ArchitectureSlide'
import GTMSlide from './slides/GTMSlide'
import RegulatorySlide from './slides/RegulatorySlide'
import EngineeringSlide from './slides/EngineeringSlide'
import AIPipelineSlide from './slides/AIPipelineSlide'
import USPSlide from './slides/USPSlide'
import DisclaimerSlide from './slides/DisclaimerSlide'
import ExecutiveSummarySlide from './slides/ExecutiveSummarySlide'
import RegulatoryLandscapeSlide from './slides/RegulatoryLandscapeSlide'
import CapTableSlide from './slides/CapTableSlide'
import SavingsSlide from './slides/SavingsSlide'
import SDKDemoSlide from './slides/SDKDemoSlide'
import StrategySlide from './slides/StrategySlide'
import FinanzplanSlide from './slides/FinanzplanSlide'
import GlossarySlide from './slides/GlossarySlide'
import RiskSlide from './slides/RiskSlide'
import MilestonesSlide from './slides/MilestonesSlide'
interface PitchDeckProps {
lang: Language
onToggleLanguage: () => void
investor: Investor | null
onLogout: () => void
previewData?: PitchData | null
}
export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout, previewData }: PitchDeckProps) {
const fetched = usePitchData()
const data = previewData || fetched.data
const loading = previewData ? false : fetched.loading
const error = previewData ? null : fetched.error
const [fabOpen, setFabOpen] = useState(false)
const isWandeldarlehen = (data?.funding?.instrument || '').toLowerCase() === 'wandeldarlehen'
const isShowcase = investor?.is_showcase === true
// Derive fp_scenario IDs from version snapshot (fm_scenarios stores fp_scenario IDs directly)
const fpScenarios = data?.fp_scenarios || []
const fpBaseScenarioId = fpScenarios.find(s => s.is_default)?.id ?? fpScenarios[0]?.id ?? null
const preferredScenarioId = fpBaseScenarioId
// Showcase mode: filter out investor/financial slides
const activeSlideOrder = isShowcase
? SLIDE_ORDER.filter(s => !SHOWCASE_HIDDEN_SLIDES.has(s))
: SLIDE_ORDER
const nav = useSlideNavigation(activeSlideOrder)
// Map active slide IDs → localized names for sidebar/overview
const i18n = t(lang)
const activeSlideNames = activeSlideOrder.map(id => {
const idx = SLIDE_ORDER.indexOf(id)
return idx >= 0 ? i18n.slideNames[idx] : id
})
// Skip cap-table slide for Wandeldarlehen versions
useEffect(() => {
if (nav.currentSlide === 'cap-table' && isWandeldarlehen) {
nav.nextSlide()
}
}, [nav.currentSlide, isWandeldarlehen, nav.nextSlide])
const presenter = usePresenterMode({
goToSlide: nav.goToSlide,
currentSlide: nav.currentIndex,
totalSlides: nav.totalSlides,
language: lang,
})
// Audit tracking
useAuditTracker({
investorId: investor?.id || null,
currentSlide: nav.currentSlide,
enabled: !!investor,
})
const toggleFullscreen = useCallback(() => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
} else {
document.exitFullscreen()
}
}, [])
const toggleMenu = useCallback(() => {
setFabOpen(prev => !prev)
}, [])
useKeyboard({
onNext: nav.nextSlide,
onPrev: nav.prevSlide,
onFirst: nav.goToFirst,
onLast: nav.goToLast,
onOverview: nav.toggleOverview,
onFullscreen: toggleFullscreen,
onLanguageToggle: onToggleLanguage,
onMenuToggle: toggleMenu,
onPresenterToggle: presenter.toggle,
onGoToSlide: nav.goToSlide,
enabled: !nav.showOverview,
})
if (loading) {
return (
<div className="h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-white/40 text-sm">{lang === 'de' ? 'Lade Pitch-Daten...' : 'Loading pitch data...'}</p>
</div>
</div>
)
}
if (error || !data) {
return (
<div className="h-screen flex items-center justify-center">
<div className="text-center max-w-md">
<p className="text-red-400 mb-2">{lang === 'de' ? 'Fehler beim Laden' : 'Loading error'}</p>
<p className="text-white/40 text-sm">{error || 'No data'}</p>
</div>
</div>
)
}
function renderSlide() {
if (!data) return null
switch (nav.currentSlide) {
case 'intro-presenter':
return (
<IntroPresenterSlide
lang={lang}
onStartPresenter={presenter.start}
isPresenting={presenter.state !== 'idle'}
/>
)
case 'executive-summary':
return <ExecutiveSummarySlide lang={lang} data={data} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} />
case 'cover':
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
case 'problem':
return <ProblemSlide lang={lang} />
case 'solution':
return <SolutionSlide lang={lang} />
case 'usp':
return <USPSlide lang={lang} />
case 'regulatory-landscape':
return <RegulatoryLandscapeSlide lang={lang} />
case 'product':
return <ProductSlide lang={lang} products={data.products} />
case 'how-it-works':
return <HowItWorksSlide lang={lang} />
case 'market':
return <MarketSlide lang={lang} market={data.market} />
case 'business-model':
return <BusinessModelSlide lang={lang} products={data.products} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} />
case 'traction':
return <MilestonesSlide lang={lang} />
case 'competition':
return <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
case 'team':
return <TeamSlide lang={lang} team={data.team} />
case 'financials':
return <FinancialsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} />
case 'the-ask':
return <TheAskSlide lang={lang} funding={data.funding} isWandeldarlehen={isWandeldarlehen} />
case 'cap-table':
if (isWandeldarlehen) return null
return <CapTableSlide lang={lang} />
case 'customer-savings':
return <SavingsSlide lang={lang} />
case 'ai-qa':
return <AIQASlide lang={lang} />
case 'annex-assumptions':
return <AssumptionsSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpScenarios={fpScenarios} />
case 'annex-architecture':
return <ArchitectureSlide lang={lang} />
case 'annex-gtm':
return <GTMSlide lang={lang} isWandeldarlehen={isWandeldarlehen} />
case 'annex-regulatory':
return <RegulatorySlide lang={lang} />
case 'annex-engineering':
return <EngineeringSlide lang={lang} />
case 'annex-aipipeline':
return <AIPipelineSlide lang={lang} />
case 'annex-sdk-demo':
return <SDKDemoSlide lang={lang} />
case 'annex-strategy':
return <StrategySlide lang={lang} isWandeldarlehen={isWandeldarlehen} />
case 'annex-finanzplan':
return <FinanzplanSlide lang={lang} investorId={investor?.id || null} preferredScenarioId={preferredScenarioId} isWandeldarlehen={isWandeldarlehen} fpBaseScenarioId={fpBaseScenarioId} fpScenarios={fpScenarios} />
case 'annex-glossary':
return <GlossarySlide lang={lang} />
case 'risks':
return <RiskSlide lang={lang} />
case 'legal-disclaimer':
return <DisclaimerSlide lang={lang} />
default:
return null
}
}
return (
<div
className="h-screen relative overflow-hidden bg-gradient-to-br from-slate-950 via-[#0a0a1a] to-slate-950 select-none"
onContextMenu={(e) => e.preventDefault()}
>
<ParticleBackground />
<ProgressBar current={nav.currentIndex} total={nav.totalSlides} />
{/* Investor watermark */}
{investor && <Watermark text={investor.email} />}
{/* Data Room link — only for real investor sessions, not preview */}
{investor && !previewData && (
<Link
href="/dataroom"
className="fixed top-4 right-4 z-40 flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-white/[0.06] border border-white/[0.08] text-white/50 hover:text-white/80 hover:bg-white/[0.1] backdrop-blur-sm transition-all text-xs"
>
<FolderOpen className="w-3.5 h-3.5" />
Data Room
</Link>
)}
<SlideContainer slideKey={nav.currentSlide} direction={nav.direction}>
{renderSlide()}
</SlideContainer>
<NavigationControls
onPrev={nav.prevSlide}
onNext={nav.nextSlide}
isFirst={nav.isFirst}
isLast={nav.isLast}
current={nav.currentIndex}
total={nav.totalSlides}
/>
<ChatFAB
lang={lang}
currentSlide={nav.currentSlide}
currentIndex={nav.currentIndex}
visitedSlides={nav.visitedSlides}
onGoToSlide={nav.goToSlide}
presenterState={presenter.state}
onPresenterInterrupt={presenter.pause}
/>
<NavigationFAB
currentIndex={nav.currentIndex}
totalSlides={nav.totalSlides}
visitedSlides={nav.visitedSlides}
onGoToSlide={nav.goToSlide}
lang={lang}
onToggleLanguage={onToggleLanguage}
slideNames={activeSlideNames}
onPresenterStart={isShowcase ? presenter.start : undefined}
/>
{/* 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}
onPrev={presenter.prevSlide}
/>
<AnimatePresence>
{nav.showOverview && (
<SlideOverview
currentIndex={nav.currentIndex}
onGoToSlide={nav.goToSlide}
onClose={() => nav.setShowOverview(false)}
lang={lang}
slideNames={activeSlideNames}
/>
)}
</AnimatePresence>
</div>
)
}