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
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:
@@ -833,8 +833,9 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DATABASE_URL: postgres://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}
|
DATABASE_URL: postgres://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db}
|
||||||
OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434}
|
LITELLM_URL: ${LITELLM_URL:-https://llm-dev.meghsakha.com}
|
||||||
OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen3.5:35b-a3b}
|
LITELLM_MODEL: ${LITELLM_MODEL:-gpt-oss-120b}
|
||||||
|
LITELLM_API_KEY: ${LITELLM_API_KEY:-sk-0nAyxaMVbIqmz_ntnndzag}
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,8 +1,36 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import pool from '@/lib/db'
|
import pool from '@/lib/db'
|
||||||
|
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
|
||||||
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
const LITELLM_URL = process.env.LITELLM_URL || 'https://llm-dev.meghsakha.com'
|
||||||
const OLLAMA_MODEL = process.env.OLLAMA_MODEL || 'qwen2.5:32b'
|
const LITELLM_MODEL = process.env.LITELLM_MODEL || 'gpt-oss-120b'
|
||||||
|
const LITELLM_API_KEY = process.env.LITELLM_API_KEY || ''
|
||||||
|
|
||||||
|
// Build SLIDE_NAMES dynamically from SLIDE_ORDER
|
||||||
|
const SLIDE_DISPLAY_NAMES: Record<string, { de: string; en: string }> = {
|
||||||
|
'intro-presenter': { de: 'Intro', en: 'Intro' },
|
||||||
|
'cover': { de: 'Cover', en: 'Cover' },
|
||||||
|
'problem': { de: 'Das Problem', en: 'The Problem' },
|
||||||
|
'solution': { de: 'Die Loesung', en: 'The Solution' },
|
||||||
|
'product': { de: 'Produkte', en: 'Products' },
|
||||||
|
'how-it-works': { de: 'So funktionierts', en: 'How It Works' },
|
||||||
|
'market': { de: 'Markt', en: 'Market' },
|
||||||
|
'business-model': { de: 'Geschaeftsmodell', en: 'Business Model' },
|
||||||
|
'traction': { de: 'Traction', en: 'Traction' },
|
||||||
|
'competition': { de: 'Wettbewerb', en: 'Competition' },
|
||||||
|
'team': { de: 'Team', en: 'Team' },
|
||||||
|
'financials': { de: 'Finanzen', en: 'Financials' },
|
||||||
|
'the-ask': { de: 'The Ask', en: 'The Ask' },
|
||||||
|
'ai-qa': { de: 'KI Q&A', en: 'AI Q&A' },
|
||||||
|
'annex-assumptions': { de: 'Anhang: Annahmen', en: 'Appendix: Assumptions' },
|
||||||
|
'annex-architecture': { de: 'Anhang: Architektur', en: 'Appendix: Architecture' },
|
||||||
|
'annex-gtm': { de: 'Anhang: Go-to-Market', en: 'Appendix: Go-to-Market' },
|
||||||
|
'annex-regulatory': { de: 'Anhang: Regulatorik', en: 'Appendix: Regulatory' },
|
||||||
|
'annex-engineering': { de: 'Anhang: Engineering', en: 'Appendix: Engineering' },
|
||||||
|
'annex-aipipeline': { de: 'Anhang: KI-Pipeline', en: 'Appendix: AI Pipeline' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const slideCount = SLIDE_ORDER.length
|
||||||
|
|
||||||
const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI
|
const SYSTEM_PROMPT = `# Investor Agent — BreakPilot ComplAI
|
||||||
|
|
||||||
@@ -40,7 +68,8 @@ Stattdessen: "Proprietaere KI-Engine", "Self-Hosted Appliance auf Apple-Hardware
|
|||||||
## Slide-Awareness (IMMER beachten)
|
## Slide-Awareness (IMMER beachten)
|
||||||
Du erhaeltst den aktuellen Slide-Kontext. Nutze ihn fuer kontextuelle Antworten.
|
Du erhaeltst den aktuellen Slide-Kontext. Nutze ihn fuer kontextuelle Antworten.
|
||||||
Wenn der Investor etwas fragt, was in einer spaeteren Slide detailliert wird und er diese noch nicht gesehen hat:
|
Wenn der Investor etwas fragt, was in einer spaeteren Slide detailliert wird und er diese noch nicht gesehen hat:
|
||||||
- Beantworte kurz, dann: "Details dazu finden Sie in Slide X: [Name]. Moechten Sie dorthin springen? [GOTO:X]"
|
- Beantworte kurz, dann: "Details dazu finden Sie in Slide X: [Name]. Moechten Sie dorthin springen? [GOTO:slide-id]"
|
||||||
|
- Verwende [GOTO:slide-id] mit der Slide-ID (z.B. [GOTO:financials], [GOTO:competition])
|
||||||
|
|
||||||
## FOLLOW-UP FRAGEN — KRITISCHE PFLICHT
|
## FOLLOW-UP FRAGEN — KRITISCHE PFLICHT
|
||||||
|
|
||||||
@@ -118,47 +147,65 @@ ${JSON.stringify(features.rows, null, 2)}
|
|||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { message, history = [], lang = 'de', slideContext } = body
|
const { message, history = [], lang = 'de', slideContext, faqAnswer } = body
|
||||||
|
|
||||||
if (!message || typeof message !== 'string') {
|
if (!message || typeof message !== 'string') {
|
||||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FAQ shortcut: if client sends a pre-cached FAQ answer, stream it directly (no LLM call)
|
||||||
|
if (faqAnswer && typeof faqAnswer === 'string') {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
// Stream the FAQ answer in chunks for consistent UX
|
||||||
|
const words = faqAnswer.split(' ')
|
||||||
|
let i = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (i < words.length) {
|
||||||
|
const chunk = (i === 0 ? '' : ' ') + words[i]
|
||||||
|
controller.enqueue(encoder.encode(chunk))
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
clearInterval(interval)
|
||||||
|
controller.close()
|
||||||
|
}
|
||||||
|
}, 30)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new NextResponse(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const pitchContext = await loadPitchContext()
|
const pitchContext = await loadPitchContext()
|
||||||
|
|
||||||
let systemContent = SYSTEM_PROMPT
|
let systemContent = SYSTEM_PROMPT
|
||||||
if (pitchContext) {
|
if (pitchContext) {
|
||||||
systemContent += '\n' + pitchContext
|
systemContent += '\n' + pitchContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slide context for contextual awareness
|
// Slide context for contextual awareness
|
||||||
if (slideContext) {
|
if (slideContext) {
|
||||||
const SLIDE_NAMES: Record<string, { de: string; en: string; index: number }> = {
|
|
||||||
'cover': { de: 'Cover', en: 'Cover', index: 0 },
|
|
||||||
'problem': { de: 'Das Problem', en: 'The Problem', index: 1 },
|
|
||||||
'solution': { de: 'Die Loesung', en: 'The Solution', index: 2 },
|
|
||||||
'product': { de: 'Produkte', en: 'Products', index: 3 },
|
|
||||||
'how-it-works': { de: 'So funktionierts', en: 'How It Works', index: 4 },
|
|
||||||
'market': { de: 'Markt', en: 'Market', index: 5 },
|
|
||||||
'business-model': { de: 'Geschaeftsmodell', en: 'Business Model', index: 6 },
|
|
||||||
'traction': { de: 'Traction', en: 'Traction', index: 7 },
|
|
||||||
'competition': { de: 'Wettbewerb', en: 'Competition', index: 8 },
|
|
||||||
'team': { de: 'Team', en: 'Team', index: 9 },
|
|
||||||
'financials': { de: 'Finanzen', en: 'Financials', index: 10 },
|
|
||||||
'the-ask': { de: 'The Ask', en: 'The Ask', index: 11 },
|
|
||||||
'ai-qa': { de: 'KI Q&A', en: 'AI Q&A', index: 12 },
|
|
||||||
}
|
|
||||||
const slideKeys = Object.keys(SLIDE_NAMES)
|
|
||||||
const visited: number[] = slideContext.visitedSlides || []
|
const visited: number[] = slideContext.visitedSlides || []
|
||||||
const currentSlideName = SLIDE_NAMES[slideContext.currentSlide]?.[lang] || slideContext.currentSlide
|
const currentSlideId = slideContext.currentSlide
|
||||||
const notYetSeen = Object.entries(SLIDE_NAMES)
|
const currentSlideName = SLIDE_DISPLAY_NAMES[currentSlideId]?.[lang] || currentSlideId
|
||||||
.filter(([, v]) => !visited.includes(v.index))
|
const notYetSeen = SLIDE_ORDER
|
||||||
.map(([, v]) => `${v.index + 1}. ${v[lang]}`)
|
.map((id, idx) => ({ id, idx, name: SLIDE_DISPLAY_NAMES[id]?.[lang] || id }))
|
||||||
|
.filter(s => !visited.includes(s.idx))
|
||||||
|
.map(s => `${s.idx + 1}. ${s.name}`)
|
||||||
|
|
||||||
systemContent += `\n\n## Slide-Kontext (WICHTIG fuer kontextuelle Antworten)
|
systemContent += `\n\n## Slide-Kontext (WICHTIG fuer kontextuelle Antworten)
|
||||||
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von 13)
|
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${slideCount})
|
||||||
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_NAMES[slideKeys[i]]?.[lang]).filter(Boolean).join(', ')}
|
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[SLIDE_ORDER[i]]?.[lang] || SLIDE_ORDER[i]).filter(Boolean).join(', ')}
|
||||||
- Noch nicht gesehene Slides: ${notYetSeen.join(', ')}
|
- Noch nicht gesehene Slides: ${notYetSeen.join(', ')}
|
||||||
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geoeffnet' : 'Nein'}
|
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geoeffnet' : 'Nein'}
|
||||||
|
- Verfuegbare Slide-IDs fuer [GOTO:id]: ${SLIDE_ORDER.join(', ')}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,54 +220,84 @@ export async function POST(request: NextRequest) {
|
|||||||
{ role: 'user', content: message + '\n\n(Erinnerung: Beende deine Antwort IMMER mit "---" gefolgt von 3 Folgefragen im Format "[Q] Frage?")' },
|
{ role: 'user', content: message + '\n\n(Erinnerung: Beende deine Antwort IMMER mit "---" gefolgt von 3 Folgefragen im Format "[Q] Frage?")' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
// LiteLLM (OpenAI-compatible API)
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
if (LITELLM_API_KEY) {
|
||||||
|
headers['Authorization'] = `Bearer ${LITELLM_API_KEY}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const llmResponse = await fetch(`${LITELLM_URL}/v1/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: OLLAMA_MODEL,
|
model: LITELLM_MODEL,
|
||||||
messages,
|
messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
think: false,
|
|
||||||
options: {
|
|
||||||
temperature: 0.4,
|
temperature: 0.4,
|
||||||
num_predict: 4096,
|
max_tokens: 4096,
|
||||||
num_ctx: 8192,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(120000),
|
signal: AbortSignal.timeout(120000),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!ollamaResponse.ok) {
|
if (!llmResponse.ok) {
|
||||||
const errorText = await ollamaResponse.text()
|
const errorText = await llmResponse.text()
|
||||||
console.error('Ollama error:', ollamaResponse.status, errorText)
|
console.error('LiteLLM error:', llmResponse.status, errorText)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}).` },
|
{ error: `LLM nicht erreichbar (Status ${llmResponse.status}).` },
|
||||||
{ status: 502 }
|
{ status: 502 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse SSE stream from LiteLLM and emit plain text to client
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const reader = ollamaResponse.body!.getReader()
|
const reader = llmResponse.body!.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
if (done) break
|
if (done) break
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
const lines = buffer.split('\n')
|
||||||
|
// Keep the last (potentially incomplete) line in the buffer
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || !trimmed.startsWith('data: ')) continue
|
||||||
|
const data = trimmed.slice(6)
|
||||||
|
if (data === '[DONE]') continue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const json = JSON.parse(line)
|
const json = JSON.parse(data)
|
||||||
if (json.message?.content) {
|
const content = json.choices?.[0]?.delta?.content
|
||||||
controller.enqueue(encoder.encode(json.message.content))
|
if (content) {
|
||||||
|
controller.enqueue(encoder.encode(content))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Partial JSON line, skip
|
// Partial JSON, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining buffer
|
||||||
|
if (buffer.trim()) {
|
||||||
|
const trimmed = buffer.trim()
|
||||||
|
if (trimmed.startsWith('data: ') && trimmed.slice(6) !== '[DONE]') {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(trimmed.slice(6))
|
||||||
|
const content = json.choices?.[0]?.delta?.content
|
||||||
|
if (content) {
|
||||||
|
controller.enqueue(encoder.encode(content))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
pitch-deck/app/api/presenter/status/route.ts
Normal file
15
pitch-deck/app/api/presenter/status/route.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter Status API — for future multi-client sync.
|
||||||
|
* Currently returns a static structure; will be connected to
|
||||||
|
* server-side presenter state when WebSocket support is added.
|
||||||
|
*/
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
state: 'idle',
|
||||||
|
currentSlide: 0,
|
||||||
|
progress: 0,
|
||||||
|
message: 'Presenter status endpoint ready. State is managed client-side for now.',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import { motion, AnimatePresence } from 'framer-motion'
|
|||||||
import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2, ArrowRight } from 'lucide-react'
|
import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2, ArrowRight } from 'lucide-react'
|
||||||
import { ChatMessage, Language, SlideId } from '@/lib/types'
|
import { ChatMessage, Language, SlideId } from '@/lib/types'
|
||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
|
import { SLIDE_ORDER } from '@/lib/hooks/useSlideNavigation'
|
||||||
|
import { PresenterState } from '@/lib/presenter/types'
|
||||||
|
import { matchFAQ, getFAQAnswer } from '@/lib/presenter/faq-matcher'
|
||||||
|
|
||||||
interface ChatFABProps {
|
interface ChatFABProps {
|
||||||
lang: Language
|
lang: Language
|
||||||
@@ -12,6 +15,8 @@ interface ChatFABProps {
|
|||||||
currentIndex: number
|
currentIndex: number
|
||||||
visitedSlides: Set<number>
|
visitedSlides: Set<number>
|
||||||
onGoToSlide: (index: number) => void
|
onGoToSlide: (index: number) => void
|
||||||
|
presenterState?: PresenterState
|
||||||
|
onPresenterInterrupt?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedMessage {
|
interface ParsedMessage {
|
||||||
@@ -62,18 +67,31 @@ function parseAgentResponse(content: string, lang: Language): ParsedMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse GOTO markers from the text
|
// Parse GOTO markers — support both [GOTO:N] (numeric) and [GOTO:slide-id] (string)
|
||||||
const gotoRegex = /\[GOTO:(\d+)\]/g
|
const gotoRegex = /\[GOTO:([\w-]+)\]/g
|
||||||
let gotoMatch
|
let gotoMatch
|
||||||
while ((gotoMatch = gotoRegex.exec(text)) !== null) {
|
while ((gotoMatch = gotoRegex.exec(text)) !== null) {
|
||||||
const slideIndex = parseInt(gotoMatch[1])
|
const target = gotoMatch[1]
|
||||||
|
let slideIndex: number
|
||||||
|
|
||||||
|
// Try numeric index first
|
||||||
|
const numericIndex = parseInt(target)
|
||||||
|
if (!isNaN(numericIndex) && numericIndex >= 0 && numericIndex < SLIDE_ORDER.length) {
|
||||||
|
slideIndex = numericIndex
|
||||||
|
} else {
|
||||||
|
// Try slide ID lookup
|
||||||
|
slideIndex = SLIDE_ORDER.indexOf(target as SlideId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slideIndex >= 0) {
|
||||||
gotos.push({
|
gotos.push({
|
||||||
index: slideIndex,
|
index: slideIndex,
|
||||||
label: lang === 'de' ? `Zu Slide ${slideIndex + 1} springen` : `Jump to slide ${slideIndex + 1}`,
|
label: lang === 'de' ? `Zu Slide ${slideIndex + 1} springen` : `Jump to slide ${slideIndex + 1}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Remove GOTO markers from visible text
|
// Remove GOTO markers from visible text
|
||||||
text = text.replace(/\s*\[GOTO:\d+\]/g, '')
|
text = text.replace(/\s*\[GOTO:[\w-]+\]/g, '')
|
||||||
|
|
||||||
// Clean up trailing reminder instruction that might leak through
|
// Clean up trailing reminder instruction that might leak through
|
||||||
text = text.replace(/\n*\(Erinnerung:.*?\)\s*$/s, '').trim()
|
text = text.replace(/\n*\(Erinnerung:.*?\)\s*$/s, '').trim()
|
||||||
@@ -81,7 +99,15 @@ function parseAgentResponse(content: string, lang: Language): ParsedMessage {
|
|||||||
return { text: text.trim(), followUps, gotos }
|
return { text: text.trim(), followUps, gotos }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlides, onGoToSlide }: ChatFABProps) {
|
export default function ChatFAB({
|
||||||
|
lang,
|
||||||
|
currentSlide,
|
||||||
|
currentIndex,
|
||||||
|
visitedSlides,
|
||||||
|
onGoToSlide,
|
||||||
|
presenterState = 'idle',
|
||||||
|
onPresenterInterrupt,
|
||||||
|
}: ChatFABProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [isExpanded, setIsExpanded] = useState(false)
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
@@ -124,18 +150,23 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
|
|||||||
const message = text || input.trim()
|
const message = text || input.trim()
|
||||||
if (!message || isStreaming) return
|
if (!message || isStreaming) return
|
||||||
|
|
||||||
|
// Interrupt presenter if it's running
|
||||||
|
if (presenterState === 'presenting' && onPresenterInterrupt) {
|
||||||
|
onPresenterInterrupt()
|
||||||
|
}
|
||||||
|
|
||||||
setInput('')
|
setInput('')
|
||||||
setMessages(prev => [...prev, { role: 'user', content: message }])
|
setMessages(prev => [...prev, { role: 'user', content: message }])
|
||||||
setIsStreaming(true)
|
setIsStreaming(true)
|
||||||
setIsWaiting(true)
|
setIsWaiting(true)
|
||||||
|
|
||||||
|
// Check FAQ first for instant response
|
||||||
|
const faqMatch = matchFAQ(message, lang)
|
||||||
|
|
||||||
abortRef.current = new AbortController()
|
abortRef.current = new AbortController()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/chat', {
|
const requestBody: Record<string, unknown> = {
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
message,
|
message,
|
||||||
history: messages.slice(-10),
|
history: messages.slice(-10),
|
||||||
lang,
|
lang,
|
||||||
@@ -143,9 +174,19 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
|
|||||||
currentSlide,
|
currentSlide,
|
||||||
currentIndex,
|
currentIndex,
|
||||||
visitedSlides: Array.from(visitedSlides),
|
visitedSlides: Array.from(visitedSlides),
|
||||||
totalSlides: 13,
|
totalSlides: SLIDE_ORDER.length,
|
||||||
},
|
},
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
// If FAQ matched, send the cached answer for fast streaming (no LLM call)
|
||||||
|
if (faqMatch) {
|
||||||
|
requestBody.faqAnswer = getFAQAnswer(faqMatch, lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch('/api/chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
signal: abortRef.current.signal,
|
signal: abortRef.current.signal,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -175,6 +216,20 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If FAQ matched and has a goto_slide, add a GOTO marker to the response
|
||||||
|
if (faqMatch?.goto_slide) {
|
||||||
|
const gotoIdx = SLIDE_ORDER.indexOf(faqMatch.goto_slide)
|
||||||
|
if (gotoIdx >= 0) {
|
||||||
|
const suffix = `\n\n[GOTO:${faqMatch.goto_slide}]`
|
||||||
|
content += suffix
|
||||||
|
setMessages(prev => {
|
||||||
|
const updated = [...prev]
|
||||||
|
updated[updated.length - 1] = { role: 'assistant', content }
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof Error && err.name === 'AbortError') return
|
if (err instanceof Error && err.name === 'AbortError') return
|
||||||
console.error('Chat error:', err)
|
console.error('Chat error:', err)
|
||||||
@@ -277,6 +332,10 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
|
|||||||
<circle cx="12" cy="10" r="1" fill="currentColor" />
|
<circle cx="12" cy="10" r="1" fill="currentColor" />
|
||||||
<circle cx="15" cy="10" r="1" fill="currentColor" />
|
<circle cx="15" cy="10" r="1" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{/* Presenter active indicator */}
|
||||||
|
{presenterState !== 'idle' && (
|
||||||
|
<span className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-green-400 border-2 border-black animate-pulse" />
|
||||||
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -307,6 +366,8 @@ export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlide
|
|||||||
<span className="text-xs text-white/30 ml-2">
|
<span className="text-xs text-white/30 ml-2">
|
||||||
{isStreaming
|
{isStreaming
|
||||||
? (lang === 'de' ? 'antwortet...' : 'responding...')
|
? (lang === 'de' ? 'antwortet...' : 'responding...')
|
||||||
|
: presenterState !== 'idle'
|
||||||
|
? (lang === 'de' ? 'Presenter aktiv' : 'Presenter active')
|
||||||
: (lang === 'de' ? 'online' : 'online')
|
: (lang === 'de' ? 'online' : 'online')
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AnimatePresence } from 'framer-motion'
|
|||||||
import { useSlideNavigation } from '@/lib/hooks/useSlideNavigation'
|
import { useSlideNavigation } from '@/lib/hooks/useSlideNavigation'
|
||||||
import { useKeyboard } from '@/lib/hooks/useKeyboard'
|
import { useKeyboard } from '@/lib/hooks/useKeyboard'
|
||||||
import { usePitchData } from '@/lib/hooks/usePitchData'
|
import { usePitchData } from '@/lib/hooks/usePitchData'
|
||||||
|
import { usePresenterMode } from '@/lib/hooks/usePresenterMode'
|
||||||
import { Language, PitchData } from '@/lib/types'
|
import { Language, PitchData } from '@/lib/types'
|
||||||
|
|
||||||
import ParticleBackground from './ParticleBackground'
|
import ParticleBackground from './ParticleBackground'
|
||||||
@@ -14,7 +15,10 @@ import NavigationFAB from './NavigationFAB'
|
|||||||
import ChatFAB from './ChatFAB'
|
import ChatFAB from './ChatFAB'
|
||||||
import SlideOverview from './SlideOverview'
|
import SlideOverview from './SlideOverview'
|
||||||
import SlideContainer from './SlideContainer'
|
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 CoverSlide from './slides/CoverSlide'
|
||||||
import ProblemSlide from './slides/ProblemSlide'
|
import ProblemSlide from './slides/ProblemSlide'
|
||||||
import SolutionSlide from './slides/SolutionSlide'
|
import SolutionSlide from './slides/SolutionSlide'
|
||||||
@@ -45,6 +49,13 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
|||||||
const nav = useSlideNavigation()
|
const nav = useSlideNavigation()
|
||||||
const [fabOpen, setFabOpen] = useState(false)
|
const [fabOpen, setFabOpen] = useState(false)
|
||||||
|
|
||||||
|
const presenter = usePresenterMode({
|
||||||
|
goToSlide: nav.goToSlide,
|
||||||
|
currentSlide: nav.currentIndex,
|
||||||
|
totalSlides: nav.totalSlides,
|
||||||
|
language: lang,
|
||||||
|
})
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
const toggleFullscreen = useCallback(() => {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
document.documentElement.requestFullscreen()
|
document.documentElement.requestFullscreen()
|
||||||
@@ -66,6 +77,7 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
|||||||
onFullscreen: toggleFullscreen,
|
onFullscreen: toggleFullscreen,
|
||||||
onLanguageToggle: onToggleLanguage,
|
onLanguageToggle: onToggleLanguage,
|
||||||
onMenuToggle: toggleMenu,
|
onMenuToggle: toggleMenu,
|
||||||
|
onPresenterToggle: presenter.toggle,
|
||||||
onGoToSlide: nav.goToSlide,
|
onGoToSlide: nav.goToSlide,
|
||||||
enabled: !nav.showOverview,
|
enabled: !nav.showOverview,
|
||||||
})
|
})
|
||||||
@@ -96,6 +108,14 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
|||||||
if (!data) return null
|
if (!data) return null
|
||||||
|
|
||||||
switch (nav.currentSlide) {
|
switch (nav.currentSlide) {
|
||||||
|
case 'intro-presenter':
|
||||||
|
return (
|
||||||
|
<IntroPresenterSlide
|
||||||
|
lang={lang}
|
||||||
|
onStartPresenter={presenter.start}
|
||||||
|
isPresenting={presenter.state !== 'idle'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
case 'cover':
|
case 'cover':
|
||||||
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
return <CoverSlide lang={lang} onNext={nav.nextSlide} funding={data.funding} />
|
||||||
case 'problem':
|
case 'problem':
|
||||||
@@ -163,6 +183,8 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
|||||||
currentIndex={nav.currentIndex}
|
currentIndex={nav.currentIndex}
|
||||||
visitedSlides={nav.visitedSlides}
|
visitedSlides={nav.visitedSlides}
|
||||||
onGoToSlide={nav.goToSlide}
|
onGoToSlide={nav.goToSlide}
|
||||||
|
presenterState={presenter.state}
|
||||||
|
onPresenterInterrupt={presenter.pause}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavigationFAB
|
<NavigationFAB
|
||||||
@@ -174,6 +196,21 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
|||||||
onToggleLanguage={onToggleLanguage}
|
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>
|
<AnimatePresence>
|
||||||
{nav.showOverview && (
|
{nav.showOverview && (
|
||||||
<SlideOverview
|
<SlideOverview
|
||||||
|
|||||||
76
pitch-deck/components/presenter/AvatarPlaceholder.tsx
Normal file
76
pitch-deck/components/presenter/AvatarPlaceholder.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { PresenterState } from '@/lib/presenter/types'
|
||||||
|
|
||||||
|
interface AvatarPlaceholderProps {
|
||||||
|
state: PresenterState
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AvatarPlaceholder({ state }: AvatarPlaceholderProps) {
|
||||||
|
const isSpeaking = state === 'presenting' || state === 'answering'
|
||||||
|
const isIdle = state === 'idle'
|
||||||
|
|
||||||
|
if (isIdle) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0, opacity: 0 }}
|
||||||
|
className="fixed bottom-24 right-6 z-40"
|
||||||
|
>
|
||||||
|
<div className="relative w-16 h-16">
|
||||||
|
{/* Pulse rings when speaking */}
|
||||||
|
{isSpeaking && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border border-indigo-400/30"
|
||||||
|
animate={{ scale: [1, 1.4, 1], opacity: [0.3, 0, 0.3] }}
|
||||||
|
transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border border-purple-400/20"
|
||||||
|
animate={{ scale: [1, 1.6, 1], opacity: [0.2, 0, 0.2] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut', delay: 0.2 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Avatar circle */}
|
||||||
|
<motion.div
|
||||||
|
className={`w-16 h-16 rounded-full flex items-center justify-center shadow-lg
|
||||||
|
${isSpeaking
|
||||||
|
? 'bg-gradient-to-br from-indigo-500/40 to-purple-500/40 border-2 border-indigo-400/50'
|
||||||
|
: 'bg-gradient-to-br from-indigo-500/20 to-purple-500/20 border border-indigo-400/30'
|
||||||
|
}`}
|
||||||
|
animate={isSpeaking ? { scale: [1, 1.05, 1] } : {}}
|
||||||
|
transition={{ duration: 0.8, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
{/* Bot icon */}
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"
|
||||||
|
className={isSpeaking ? 'text-indigo-300' : 'text-indigo-400/60'}
|
||||||
|
>
|
||||||
|
<rect x="3" y="11" width="18" height="10" rx="2" />
|
||||||
|
<circle cx="12" cy="5" r="2" />
|
||||||
|
<path d="M12 7v4" />
|
||||||
|
<circle cx="8" cy="16" r="1" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="16" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* State dot */}
|
||||||
|
<div className={`absolute -top-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-black/80
|
||||||
|
${state === 'presenting' ? 'bg-green-400' :
|
||||||
|
state === 'paused' ? 'bg-yellow-400' :
|
||||||
|
state === 'answering' ? 'bg-blue-400' :
|
||||||
|
'bg-indigo-400'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
161
pitch-deck/components/presenter/PresenterOverlay.tsx
Normal file
161
pitch-deck/components/presenter/PresenterOverlay.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
'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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { Language, PitchFeature, PitchCompetitor } from '@/lib/types'
|
|||||||
import { t } from '@/lib/i18n'
|
import { t } from '@/lib/i18n'
|
||||||
import {
|
import {
|
||||||
ChevronDown, ChevronRight, Globe, Building2, Users, TrendingUp,
|
ChevronDown, ChevronRight, Globe, Building2, Users, TrendingUp,
|
||||||
DollarSign, Cpu, Star, Check, X, Minus,
|
DollarSign, Cpu, Star, Check, X, Minus, Shield, Tag,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
@@ -201,6 +201,12 @@ const ALL_FEATURES: ComparisonFeature[] = [
|
|||||||
{ de: 'SBOM-Generator (CycloneDX/SPDX)', en: 'SBOM Generator (CycloneDX/SPDX)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
{ de: 'SBOM-Generator (CycloneDX/SPDX)', en: 'SBOM Generator (CycloneDX/SPDX)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
{ de: 'Multi-Framework Consent SDK', en: 'Multi-Framework Consent SDK', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
{ de: 'Multi-Framework Consent SDK', en: 'Multi-Framework Consent SDK', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
{ de: 'RAG mit 2.274 Rechtstexten', en: 'RAG with 2,274 Legal Texts', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
{ de: 'RAG mit 2.274 Rechtstexten', en: 'RAG with 2,274 Legal Texts', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
|
// Pentesting & Code-Security (kein Compliance-Wettbewerber hat dies)
|
||||||
|
{ de: 'SAST (Static Application Security Testing)', en: 'SAST (Static Application Security Testing)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
|
{ de: 'DAST (Dynamic Application Security Testing)', en: 'DAST (Dynamic Application Security Testing)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
|
{ de: 'LLM-Auto-Fix (automatische Code-Korrekturen)', en: 'LLM Auto-Fix (Automatic Code Corrections)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
|
{ de: 'Container-Security Scanning (Trivy)', en: 'Container Security Scanning (Trivy)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
|
{ de: 'Secret Detection (Gitleaks)', en: 'Secret Detection (Gitleaks)', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: true },
|
||||||
// Compliance Features (shared)
|
// Compliance Features (shared)
|
||||||
{ de: 'DSGVO / GDPR', en: 'GDPR', bp: true, vanta: 'partial', drata: 'partial', sprinto: 'partial', proliance: true, dataguard: true, heydata: true, isDiff: false, isUSP: false },
|
{ de: 'DSGVO / GDPR', en: 'GDPR', bp: true, vanta: 'partial', drata: 'partial', sprinto: 'partial', proliance: true, dataguard: true, heydata: true, isDiff: false, isUSP: false },
|
||||||
{ de: 'AI Act', en: 'AI Act', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: false },
|
{ de: 'AI Act', en: 'AI Act', bp: true, vanta: false, drata: false, sprinto: false, proliance: false, dataguard: false, heydata: false, isDiff: false, isUSP: false },
|
||||||
@@ -248,6 +254,179 @@ const DACH_NOTE = {
|
|||||||
en: 'Other DACH players: Secjur (Hamburg, AI compliance, ~€5.5M seed), Usercentrics (CMP only, $117M rev), Caralegal (privacy/risk, M&A 2025), 2B Advice (legacy, 20+ yrs), OneTrust (US enterprise, $500M+ ARR). None combines GDPR + code security + self-hosted AI.',
|
en: 'Other DACH players: Secjur (Hamburg, AI compliance, ~€5.5M seed), Usercentrics (CMP only, $117M rev), Caralegal (privacy/risk, M&A 2025), 2B Advice (legacy, 20+ yrs), OneTrust (US enterprise, $500M+ ARR). None combines GDPR + code security + self-hosted AI.',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Pricing Comparison Data ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface PricingTier {
|
||||||
|
name: { de: string; en: string }
|
||||||
|
price: string
|
||||||
|
annual: string
|
||||||
|
notes: { de: string; en: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CompetitorPricing {
|
||||||
|
name: string
|
||||||
|
flag: string
|
||||||
|
model: string
|
||||||
|
publicPricing: boolean
|
||||||
|
tiers: PricingTier[]
|
||||||
|
setupFee: string
|
||||||
|
isBP?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRICING_COMPARISON: CompetitorPricing[] = [
|
||||||
|
{
|
||||||
|
name: 'ComplAI',
|
||||||
|
flag: '🇩🇪',
|
||||||
|
model: 'Self-Hosted',
|
||||||
|
publicPricing: true,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Starter', en: 'Starter' }, price: '€990/mo', annual: '€11.880/yr', notes: { de: 'Mac Mini, 30B LLM, 57 Module', en: 'Mac Mini, 30B LLM, 57 modules' } },
|
||||||
|
{ name: { de: 'Professional', en: 'Professional' }, price: '€1.490/mo', annual: '€17.880/yr', notes: { de: 'Mac Studio, 70B LLM, Priority', en: 'Mac Studio, 70B LLM, priority' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '€2.990/mo', annual: '€35.880/yr', notes: { de: '2x Mac Studio, 1000B Cloud-LLM', en: '2x Mac Studio, 1000B cloud LLM' } },
|
||||||
|
],
|
||||||
|
setupFee: '€0',
|
||||||
|
isBP: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Vanta',
|
||||||
|
flag: '🇺🇸',
|
||||||
|
model: 'SaaS',
|
||||||
|
publicPricing: false,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Startup', en: 'Startup' }, price: '~$500/mo', annual: '~$6K/yr', notes: { de: '1 Framework, <50 MA', en: '1 framework, <50 employees' } },
|
||||||
|
{ name: { de: 'Business', en: 'Business' }, price: '~$2K/mo', annual: '~$25K/yr', notes: { de: 'Multi-Framework, VRM', en: 'Multi-framework, VRM' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '~$5-7K/mo', annual: '~$60-80K/yr', notes: { de: 'Custom, SSO, RBAC', en: 'Custom, SSO, RBAC' } },
|
||||||
|
],
|
||||||
|
setupFee: '~$5-15K',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Drata',
|
||||||
|
flag: '🇺🇸',
|
||||||
|
model: 'SaaS',
|
||||||
|
publicPricing: false,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Foundation', en: 'Foundation' }, price: '~$500/mo', annual: '~$5-8K/yr', notes: { de: '1 Framework, Basis', en: '1 framework, basic' } },
|
||||||
|
{ name: { de: 'Business', en: 'Business' }, price: '~$1.5K/mo', annual: '~$18-20K/yr', notes: { de: 'Multi-Framework, API', en: 'Multi-framework, API' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '~$4-8K/mo', annual: '~$50-100K/yr', notes: { de: 'SafeBase, Custom', en: 'SafeBase, custom' } },
|
||||||
|
],
|
||||||
|
setupFee: '~$5-10K',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sprinto',
|
||||||
|
flag: '🇮🇳',
|
||||||
|
model: 'SaaS',
|
||||||
|
publicPricing: false,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Growth', en: 'Growth' }, price: '~$350/mo', annual: '~$4K/yr', notes: { de: '1 Framework, KMU', en: '1 framework, SMB' } },
|
||||||
|
{ name: { de: 'Business', en: 'Business' }, price: '~$1K/mo', annual: '~$12K/yr', notes: { de: 'Multi-Framework', en: 'Multi-framework' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '~$2K+/mo', annual: '~$25K+/yr', notes: { de: 'Custom Integrations', en: 'Custom integrations' } },
|
||||||
|
],
|
||||||
|
setupFee: '~$2-5K',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Proliance',
|
||||||
|
flag: '🇩🇪',
|
||||||
|
model: 'SaaS',
|
||||||
|
publicPricing: true,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Basis', en: 'Basic' }, price: '€99/mo', annual: '€1.188/yr', notes: { de: 'DSGVO-Grundlagen', en: 'GDPR basics' } },
|
||||||
|
{ name: { de: 'Professional', en: 'Professional' }, price: '€249/mo', annual: '€2.988/yr', notes: { de: '+ Audit, VVT', en: '+ Audit, records' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '€499/mo', annual: '€5.988/yr', notes: { de: 'Multi-Standort, DSB', en: 'Multi-location, DPO' } },
|
||||||
|
],
|
||||||
|
setupFee: '€0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'DataGuard',
|
||||||
|
flag: '🇩🇪',
|
||||||
|
model: 'SaaS + Beratung',
|
||||||
|
publicPricing: false,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Starter', en: 'Starter' }, price: '~€250/mo', annual: '~€3K/yr', notes: { de: 'Nur Software', en: 'Software only' } },
|
||||||
|
{ name: { de: 'Managed', en: 'Managed' }, price: '~€1K/mo', annual: '~€12K/yr', notes: { de: '+ Ext. DSB', en: '+ Ext. DPO' } },
|
||||||
|
{ name: { de: 'Enterprise', en: 'Enterprise' }, price: '~€2K+/mo', annual: '~€24K+/yr', notes: { de: 'ISO 27001 + TISAX', en: 'ISO 27001 + TISAX' } },
|
||||||
|
],
|
||||||
|
setupFee: '~€2-5K',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heyData',
|
||||||
|
flag: '🇩🇪',
|
||||||
|
model: 'SaaS',
|
||||||
|
publicPricing: true,
|
||||||
|
tiers: [
|
||||||
|
{ name: { de: 'Essential', en: 'Essential' }, price: '€83/mo', annual: '€996/yr', notes: { de: '1-19 MA, DSGVO', en: '1-19 empl., GDPR' } },
|
||||||
|
{ name: { de: 'Pro', en: 'Pro' }, price: '€199/mo', annual: '€2.388/yr', notes: { de: '20-99 MA, DSB', en: '20-99 empl., DPO' } },
|
||||||
|
{ name: { de: 'Premium', en: 'Premium' }, price: '€333/mo', annual: '€3.996/yr', notes: { de: '100+ MA, Audit', en: '100+ empl., audit' } },
|
||||||
|
],
|
||||||
|
setupFee: '€0',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─── AppSec / Pentesting Competitor Data ─────────────────────────────────────
|
||||||
|
|
||||||
|
interface AppSecCompetitor {
|
||||||
|
name: string
|
||||||
|
flag: string
|
||||||
|
hq: string
|
||||||
|
founded: number
|
||||||
|
employees: number
|
||||||
|
revenue: string
|
||||||
|
revenueNum: number
|
||||||
|
customers: string
|
||||||
|
funding: string
|
||||||
|
pricing: string
|
||||||
|
focus: { de: string; en: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
const APPSEC_COMPETITORS: AppSecCompetitor[] = [
|
||||||
|
{ name: 'Snyk', flag: '🇺🇸', hq: 'Boston', founded: 2015, employees: 1200, revenue: '~$300M ARR', revenueNum: 300_000_000, customers: '3.000+', funding: '$850M (Series G, $7.4B)', pricing: '$25K–100K+/yr', focus: { de: 'SCA + SAST, Developer-First', en: 'SCA + SAST, developer-first' } },
|
||||||
|
{ name: 'Veracode', flag: '🇺🇸', hq: 'Burlington, MA', founded: 2006, employees: 1300, revenue: '~$300M', revenueNum: 300_000_000, customers: '3.500+', funding: 'PE (Thoma Bravo, $2.5B)', pricing: '$50K–500K+/yr', focus: { de: 'SAST + DAST + SCA, Enterprise', en: 'SAST + DAST + SCA, enterprise' } },
|
||||||
|
{ name: 'Checkmarx', flag: '🇮🇱', hq: 'Tel Aviv', founded: 2006, employees: 1000, revenue: '~$250M', revenueNum: 250_000_000, customers: '1.800+', funding: 'PE (Hellman & Friedman)', pricing: '$40K–300K+/yr', focus: { de: 'SAST + DAST + SCA + API', en: 'SAST + DAST + SCA + API' } },
|
||||||
|
{ name: 'SonarSource', flag: '🇨🇭', hq: 'Genf', founded: 2008, employees: 500, revenue: '~$250M', revenueNum: 250_000_000, customers: '400K+ Devs', funding: '$412M (Series D)', pricing: '$15K–150K+/yr', focus: { de: 'Code-Qualitaet + SAST', en: 'Code quality + SAST' } },
|
||||||
|
{ name: 'Semgrep', flag: '🇺🇸', hq: 'San Francisco', founded: 2020, employees: 150, revenue: '~$30M ARR', revenueNum: 30_000_000, customers: '1.500+', funding: '$100M (Series C)', pricing: '$10K–100K+/yr', focus: { de: 'Open-Source SAST, Supply Chain', en: 'Open-source SAST, supply chain' } },
|
||||||
|
{ name: 'Pentera', flag: '🇮🇱', hq: 'Tel Aviv', founded: 2015, employees: 400, revenue: '~$100M', revenueNum: 100_000_000, customers: '900+', funding: '$189M (Series C)', pricing: '$50K–250K+/yr', focus: { de: 'Automatisiertes Pentesting/BAS', en: 'Automated pentesting/BAS' } },
|
||||||
|
{ name: 'Invicti', flag: '🇺🇸', hq: 'Austin, TX', founded: 2018, employees: 500, revenue: '~$100M', revenueNum: 100_000_000, customers: '3.000+', funding: 'PE (Turn/River)', pricing: '$15K–100K+/yr', focus: { de: 'DAST (Acunetix + Netsparker)', en: 'DAST (Acunetix + Netsparker)' } },
|
||||||
|
{ name: 'Intruder', flag: '🇬🇧', hq: 'London', founded: 2015, employees: 100, revenue: '~$10M', revenueNum: 10_000_000, customers: '2.500+', funding: '$15M (Series A)', pricing: '$1.5K–20K+/yr', focus: { de: 'Vulnerability Scanner, SMB', en: 'Vulnerability scanner, SMB' } },
|
||||||
|
]
|
||||||
|
|
||||||
|
interface AppSecFeature {
|
||||||
|
de: string
|
||||||
|
en: string
|
||||||
|
bp: FeatureStatus
|
||||||
|
snyk: FeatureStatus
|
||||||
|
veracode: FeatureStatus
|
||||||
|
checkmarx: FeatureStatus
|
||||||
|
sonar: FeatureStatus
|
||||||
|
semgrep: FeatureStatus
|
||||||
|
pentera: FeatureStatus
|
||||||
|
invicti: FeatureStatus
|
||||||
|
intruder: FeatureStatus
|
||||||
|
isUSP: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const APPSEC_FEATURES: AppSecFeature[] = [
|
||||||
|
// ComplAI USPs — kein AppSec-Anbieter hat dies
|
||||||
|
{ de: 'DSGVO / GDPR Compliance', en: 'GDPR Compliance', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: 'AI Act Compliance', en: 'AI Act Compliance', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: 'CRA & NIS2 Compliance', en: 'CRA & NIS2 Compliance', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: '57 Compliance-Module (SDK)', en: '57 Compliance Modules (SDK)', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: 'Self-Hosted KI (On-Premise)', en: 'Self-Hosted AI (On-Premise)', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: 'PII-Redaction LLM Gateway', en: 'PII Redaction LLM Gateway', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
{ de: 'Firmware & Embedded-Security', en: 'Firmware & Embedded Security', bp: true, snyk: false, veracode: 'partial', checkmarx: false, sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: true },
|
||||||
|
// Shared AppSec Features
|
||||||
|
{ de: 'SAST (Static Analysis)', en: 'SAST (Static Analysis)', bp: true, snyk: true, veracode: true, checkmarx: true, sonar: true, semgrep: true, pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'DAST (Dynamic Analysis)', en: 'DAST (Dynamic Analysis)', bp: true, snyk: false, veracode: true, checkmarx: true, sonar: false, semgrep: false, pentera: true, invicti: true, intruder: true, isUSP: false },
|
||||||
|
{ de: 'SCA (Software Composition)', en: 'SCA (Software Composition)', bp: true, snyk: true, veracode: true, checkmarx: true, sonar: 'partial', semgrep: 'partial', pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'LLM-basierte Auto-Fixes', en: 'LLM-Based Auto-Fixes', bp: true, snyk: 'partial', veracode: 'partial', checkmarx: 'partial', sonar: 'partial', semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'SBOM-Generierung', en: 'SBOM Generation', bp: true, snyk: true, veracode: 'partial', checkmarx: 'partial', sonar: false, semgrep: false, pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'Container-Security', en: 'Container Security', bp: true, snyk: true, veracode: true, checkmarx: true, sonar: false, semgrep: 'partial', pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'Secret Detection', en: 'Secret Detection', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: 'partial', semgrep: true, pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'IaC Scanning', en: 'IaC Scanning', bp: true, snyk: true, veracode: false, checkmarx: false, sonar: false, semgrep: true, pentera: false, invicti: false, intruder: false, isUSP: false },
|
||||||
|
{ de: 'CI/CD-Integration', en: 'CI/CD Integration', bp: true, snyk: true, veracode: true, checkmarx: true, sonar: true, semgrep: true, pentera: 'partial', invicti: 'partial', intruder: 'partial', isUSP: false },
|
||||||
|
{ de: 'API-Security Testing', en: 'API Security Testing', bp: true, snyk: false, veracode: 'partial', checkmarx: true, sonar: false, semgrep: false, pentera: 'partial', invicti: true, intruder: 'partial', isUSP: false },
|
||||||
|
{ de: 'Automatisiertes Pentesting', en: 'Automated Pentesting', bp: true, snyk: false, veracode: false, checkmarx: false, sonar: false, semgrep: false, pentera: true, invicti: false, intruder: true, isUSP: false },
|
||||||
|
{ de: 'Self-Hosted / On-Premise', en: 'Self-Hosted / On-Premise', bp: true, snyk: false, veracode: false, checkmarx: 'partial', sonar: true, semgrep: 'partial', pentera: 'partial', invicti: 'partial', intruder: false, isUSP: false },
|
||||||
|
]
|
||||||
|
|
||||||
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function StatusIcon({ value }: { value: FeatureStatus }) {
|
function StatusIcon({ value }: { value: FeatureStatus }) {
|
||||||
@@ -304,7 +483,7 @@ function SectionHeader({
|
|||||||
|
|
||||||
// ─── Component ─────────────────────────────────────────────────────────────────
|
// ─── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type ViewTab = 'overview' | 'features'
|
type ViewTab = 'overview' | 'features' | 'pricing' | 'appsec'
|
||||||
|
|
||||||
export default function CompetitionSlide({ lang, features, competitors }: CompetitionSlideProps) {
|
export default function CompetitionSlide({ lang, features, competitors }: CompetitionSlideProps) {
|
||||||
const i = t(lang)
|
const i = t(lang)
|
||||||
@@ -344,10 +523,12 @@ export default function CompetitionSlide({ lang, features, competitors }: Compet
|
|||||||
</FadeInView>
|
</FadeInView>
|
||||||
|
|
||||||
{/* Tab Bar */}
|
{/* Tab Bar */}
|
||||||
<FadeInView delay={0.15} className="flex justify-center gap-2 mb-4">
|
<FadeInView delay={0.15} className="flex justify-center gap-2 mb-4 flex-wrap">
|
||||||
{([
|
{([
|
||||||
{ key: 'overview' as ViewTab, de: 'Ueberblick & Vergleich', en: 'Overview & Comparison' },
|
{ key: 'overview' as ViewTab, de: 'Ueberblick & Vergleich', en: 'Overview & Comparison' },
|
||||||
{ key: 'features' as ViewTab, de: 'Feature-Matrix (Detail)', en: 'Feature Matrix (Detail)' },
|
{ key: 'features' as ViewTab, de: 'Feature-Matrix (Detail)', en: 'Feature Matrix (Detail)' },
|
||||||
|
{ key: 'pricing' as ViewTab, de: 'Pricing-Vergleich', en: 'Pricing Comparison' },
|
||||||
|
{ key: 'appsec' as ViewTab, de: 'Pentesting & AppSec', en: 'Pentesting & AppSec' },
|
||||||
]).map(tab => (
|
]).map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
@@ -509,6 +690,178 @@ export default function CompetitionSlide({ lang, features, competitors }: Compet
|
|||||||
</div>
|
</div>
|
||||||
</FadeInView>
|
</FadeInView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* ─── Tab: Pricing ─── */}
|
||||||
|
{activeTab === 'pricing' && (
|
||||||
|
<FadeInView delay={0.2}>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-[11px] border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-white/10">
|
||||||
|
<th className="text-left py-2 px-2 text-white/40 font-medium min-w-[90px]">{lang === 'de' ? 'Anbieter' : 'Provider'}</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Modell' : 'Model'}</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Einstieg' : 'Entry'}</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Mid</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Enterprise</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">Setup</th>
|
||||||
|
<th className="py-2 px-1.5 text-white/40 font-medium text-center">{lang === 'de' ? 'Oeffentlich' : 'Public'}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{PRICING_COMPARISON.map((cp) => (
|
||||||
|
<tr key={cp.name} className={`border-b border-white/5 ${cp.isBP ? 'bg-indigo-500/5' : ''}`}>
|
||||||
|
<td className="py-2 px-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span>{cp.flag}</span>
|
||||||
|
<span className={`font-semibold ${cp.isBP ? 'text-indigo-400' : 'text-white/70'}`}>
|
||||||
|
{cp.isBP ? <BrandName className="text-[11px]" /> : cp.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-1.5 text-center">
|
||||||
|
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${cp.model === 'Self-Hosted' ? 'bg-green-500/15 text-green-400' : 'bg-white/5 text-white/40'}`}>
|
||||||
|
{cp.model}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{cp.tiers.map((tier, idx) => (
|
||||||
|
<td key={idx} className="py-2 px-1.5 text-center">
|
||||||
|
<div className={`font-semibold ${cp.isBP ? 'text-indigo-300' : 'text-white/70'}`}>{tier.price}</div>
|
||||||
|
<div className="text-[10px] text-white/30">{tier.annual}</div>
|
||||||
|
<div className="text-[10px] text-white/25 mt-0.5">{lang === 'de' ? tier.notes.de : tier.notes.en}</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="py-2 px-1.5 text-center text-white/40">{cp.setupFee}</td>
|
||||||
|
<td className="py-2 px-1.5 text-center">
|
||||||
|
{cp.publicPricing
|
||||||
|
? <Check className="w-3.5 h-3.5 text-green-400 mx-auto" />
|
||||||
|
: <X className="w-3.5 h-3.5 text-white/15 mx-auto" />
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Insights */}
|
||||||
|
<GlassCard className="!p-3 mt-4" hover={false}>
|
||||||
|
<h4 className="text-xs font-semibold text-white/60 mb-2 flex items-center gap-1.5">
|
||||||
|
<Tag className="w-3.5 h-3.5" />
|
||||||
|
{lang === 'de' ? 'Pricing-Einordnung' : 'Pricing Context'}
|
||||||
|
</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-[11px]">
|
||||||
|
<div className="bg-white/[0.03] rounded-lg p-2">
|
||||||
|
<div className="text-white/50 mb-1">{lang === 'de' ? 'Compliance-Only (DACH)' : 'Compliance Only (DACH)'}</div>
|
||||||
|
<div className="text-white/80 font-medium">€83 – €499/mo</div>
|
||||||
|
<div className="text-white/30 text-[10px]">{lang === 'de' ? 'Proliance, heyData — nur DSGVO, kein Code-Security' : 'Proliance, heyData — GDPR only, no code security'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/[0.03] rounded-lg p-2">
|
||||||
|
<div className="text-white/50 mb-1">{lang === 'de' ? 'US-Enterprise (Global)' : 'US Enterprise (Global)'}</div>
|
||||||
|
<div className="text-white/80 font-medium">$500 – $7K+/mo</div>
|
||||||
|
<div className="text-white/30 text-[10px]">{lang === 'de' ? 'Vanta, Drata — SOC 2 Fokus, Setup-Gebuehr, kein Self-Hosted' : 'Vanta, Drata — SOC 2 focus, setup fee, no self-hosted'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-lg p-2">
|
||||||
|
<div className="text-indigo-400 mb-1 font-medium">ComplAI</div>
|
||||||
|
<div className="text-white/80 font-medium">€990 – €2.990/mo</div>
|
||||||
|
<div className="text-white/30 text-[10px]">{lang === 'de' ? 'Compliance + Code-Security + Self-Hosted KI, kein Setup' : 'Compliance + code security + self-hosted AI, no setup fee'}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/[0.03] rounded-lg p-2">
|
||||||
|
<div className="text-white/50 mb-1">{lang === 'de' ? 'AppSec-Tools (separat)' : 'AppSec Tools (separate)'}</div>
|
||||||
|
<div className="text-white/80 font-medium">$10K – $500K+/yr</div>
|
||||||
|
<div className="text-white/30 text-[10px]">{lang === 'de' ? 'Snyk, Veracode — keine Compliance, Cloud-only' : 'Snyk, Veracode — no compliance, cloud only'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-white/25 text-center mt-3 italic">
|
||||||
|
{lang === 'de'
|
||||||
|
? '~ = geschaetzte Preise (nicht oeffentlich). Alle Preise ohne MwSt. Stand: Q1 2026.'
|
||||||
|
: '~ = estimated pricing (not public). All prices excl. VAT. As of Q1 2026.'}
|
||||||
|
</p>
|
||||||
|
</FadeInView>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ─── Tab: AppSec / Pentesting ─── */}
|
||||||
|
{activeTab === 'appsec' && (
|
||||||
|
<FadeInView delay={0.2}>
|
||||||
|
{/* Intro */}
|
||||||
|
<GlassCard className="!p-3 mb-4" hover={false}>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<Shield className="w-4 h-4 text-red-400 mt-0.5 shrink-0" />
|
||||||
|
<div className="text-[11px]">
|
||||||
|
<span className="text-white/80 font-semibold">
|
||||||
|
{lang === 'de' ? 'Warum ein 2. Wettbewerbsvergleich?' : 'Why a 2nd competitive comparison?'}
|
||||||
|
</span>
|
||||||
|
<p className="text-white/50 mt-1 leading-relaxed">
|
||||||
|
{lang === 'de'
|
||||||
|
? 'Kein Compliance-Anbieter (Vanta, Drata, etc.) bietet DAST, SAST oder LLM-basierte Code-Fixes. Kein AppSec-Anbieter (Snyk, Veracode, etc.) bietet DSGVO/AI-Act-Compliance. ComplAI ist die einzige Plattform, die beides kombiniert.'
|
||||||
|
: 'No compliance vendor (Vanta, Drata, etc.) offers DAST, SAST, or LLM-based code fixes. No AppSec vendor (Snyk, Veracode, etc.) offers GDPR/AI Act compliance. ComplAI is the only platform combining both.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
|
||||||
|
{/* AppSec Competitor Cards */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Shield className="w-3.5 h-3.5 text-red-400" />
|
||||||
|
<span className="text-xs font-semibold text-red-400">{lang === 'de' ? 'AppSec / Pentesting Anbieter' : 'AppSec / Pentesting Providers'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{APPSEC_COMPETITORS.map(c => (
|
||||||
|
<AppSecCard key={c.name} competitor={c} lang={lang} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AppSec Feature Matrix */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<SectionHeader
|
||||||
|
label={lang === 'de' ? 'USP — nur ComplAI' : 'USP — ComplAI only'}
|
||||||
|
count={APPSEC_FEATURES.filter(f => f.isUSP).length}
|
||||||
|
open={openSections.has('appsec-usp')}
|
||||||
|
onToggle={() => toggleSection('appsec-usp')}
|
||||||
|
accent="text-indigo-400"
|
||||||
|
/>
|
||||||
|
{openSections.has('appsec-usp') && (
|
||||||
|
<AppSecFeatureTable features={APPSEC_FEATURES.filter(f => f.isUSP)} lang={lang} highlight />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<SectionHeader
|
||||||
|
label={lang === 'de' ? 'Alle AppSec Features' : 'All AppSec Features'}
|
||||||
|
count={APPSEC_FEATURES.length}
|
||||||
|
open={openSections.has('appsec-all')}
|
||||||
|
onToggle={() => toggleSection('appsec-all')}
|
||||||
|
/>
|
||||||
|
{openSections.has('appsec-all') && (
|
||||||
|
<AppSecFeatureTable features={APPSEC_FEATURES} lang={lang} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Score Summary */}
|
||||||
|
<div className="mt-4 flex items-center justify-center gap-4 flex-wrap">
|
||||||
|
{[
|
||||||
|
{ name: 'ComplAI', score: APPSEC_FEATURES.filter(f => f.bp === true).length, color: 'text-indigo-400' },
|
||||||
|
{ name: 'Snyk', score: APPSEC_FEATURES.filter(f => f.snyk === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Veracode', score: APPSEC_FEATURES.filter(f => f.veracode === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Checkmarx', score: APPSEC_FEATURES.filter(f => f.checkmarx === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'SonarSrc', score: APPSEC_FEATURES.filter(f => f.sonar === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Semgrep', score: APPSEC_FEATURES.filter(f => f.semgrep === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Pentera', score: APPSEC_FEATURES.filter(f => f.pentera === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Invicti', score: APPSEC_FEATURES.filter(f => f.invicti === true).length, color: 'text-white/50' },
|
||||||
|
{ name: 'Intruder', score: APPSEC_FEATURES.filter(f => f.intruder === true).length, color: 'text-white/50' },
|
||||||
|
].map(item => (
|
||||||
|
<div key={item.name} className="text-center">
|
||||||
|
<div className={`text-lg font-bold ${item.color}`}>{item.score}/{APPSEC_FEATURES.length}</div>
|
||||||
|
<div className="text-[10px] text-white/40">{item.name}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FadeInView>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -619,3 +972,71 @@ function FeatureTable({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppSecCard({ competitor: c, lang }: { competitor: AppSecCompetitor; lang: Language }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white/[0.04] border border-white/5 rounded-xl p-2 text-[11px]">
|
||||||
|
<div className="flex items-center gap-1.5 mb-1">
|
||||||
|
<span className="text-sm">{c.flag}</span>
|
||||||
|
<span className="font-semibold text-white/80 text-xs">{c.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-white/40 mb-1 truncate">{c.hq} · {c.founded}</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-2 gap-y-0.5 text-white/50">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Users className="w-2.5 h-2.5 text-white/30" />
|
||||||
|
<span className="text-white/70">{c.employees.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<DollarSign className="w-2.5 h-2.5 text-white/30" />
|
||||||
|
<span className="text-white/70 truncate">{c.revenue}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 pt-1 border-t border-white/5 text-[10px]">
|
||||||
|
<div className="text-white/40 truncate">{c.funding}</div>
|
||||||
|
<div className="text-white/50 mt-0.5">{c.pricing}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-[10px] text-white/35 truncate" title={c.focus[lang]}>
|
||||||
|
{c.focus[lang]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppSecFeatureTable({ features, lang, highlight }: { features: AppSecFeature[]; lang: Language; highlight?: boolean }) {
|
||||||
|
const cols = ['bp', 'snyk', 'veracode', 'checkmarx', 'sonar', 'semgrep', 'pentera', 'invicti', 'intruder'] as const
|
||||||
|
const labels = ['ComplAI', 'Snyk', 'Veracode', 'Checkmarx', 'Sonar', 'Semgrep', 'Pentera', 'Invicti', 'Intruder']
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-x-auto mt-1 mb-1">
|
||||||
|
<table className="w-full text-[11px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-white/10">
|
||||||
|
<th className="text-left py-1.5 px-2 text-white/40 font-medium min-w-[160px]">Feature</th>
|
||||||
|
{labels.map((l, idx) => (
|
||||||
|
<th key={l} className={`py-1.5 px-1 font-medium text-center whitespace-nowrap ${idx === 0 ? 'text-indigo-400' : 'text-white/50'}`}>
|
||||||
|
{idx === 0 ? <BrandName className="text-[11px]" /> : l}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{features.map((f, i) => (
|
||||||
|
<tr key={i} className={`border-b border-white/5 ${highlight && f.isUSP ? 'bg-indigo-500/5' : ''}`}>
|
||||||
|
<td className="py-1.5 px-2 flex items-center gap-1.5">
|
||||||
|
{f.isUSP && highlight && <Star className="w-3 h-3 text-yellow-400 shrink-0" />}
|
||||||
|
<span className={f.isUSP && highlight ? 'text-white font-medium' : 'text-white/60'}>
|
||||||
|
{lang === 'de' ? f.de : f.en}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{cols.map(col => (
|
||||||
|
<td key={col} className="py-1.5 px-1 text-center">
|
||||||
|
<StatusIcon value={f[col] as FeatureStatus} />
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
116
pitch-deck/components/slides/IntroPresenterSlide.tsx
Normal file
116
pitch-deck/components/slides/IntroPresenterSlide.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Play, MessageCircle, Pause } from 'lucide-react'
|
||||||
|
import { Language } from '@/lib/types'
|
||||||
|
import GradientText from '../ui/GradientText'
|
||||||
|
|
||||||
|
interface IntroPresenterSlideProps {
|
||||||
|
lang: Language
|
||||||
|
onStartPresenter?: () => void
|
||||||
|
isPresenting?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IntroPresenterSlide({ lang, onStartPresenter, isPresenting }: IntroPresenterSlideProps) {
|
||||||
|
const isDE = lang === 'de'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center px-8 text-center">
|
||||||
|
{/* Avatar Placeholder Circle */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
className="relative mb-8"
|
||||||
|
>
|
||||||
|
<div className="w-32 h-32 rounded-full bg-gradient-to-br from-indigo-500/30 to-purple-500/30 border-2 border-indigo-400/40 flex items-center justify-center">
|
||||||
|
{/* Pulse rings */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border-2 border-indigo-400/20"
|
||||||
|
animate={{ scale: [1, 1.3, 1], opacity: [0.4, 0, 0.4] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 rounded-full border-2 border-purple-400/20"
|
||||||
|
animate={{ scale: [1, 1.5, 1], opacity: [0.3, 0, 0.3] }}
|
||||||
|
transition={{ duration: 2.5, repeat: Infinity, ease: 'easeInOut', delay: 0.3 }}
|
||||||
|
/>
|
||||||
|
{/* Bot icon */}
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-indigo-300">
|
||||||
|
<rect x="3" y="11" width="18" height="10" rx="2" />
|
||||||
|
<circle cx="12" cy="5" r="2" />
|
||||||
|
<path d="M12 7v4" />
|
||||||
|
<circle cx="8" cy="16" r="1" fill="currentColor" />
|
||||||
|
<circle cx="16" cy="16" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
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>
|
||||||
|
</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.'
|
||||||
|
: 'Your personal AI guide through the BreakPilot ComplAI pitch deck. 15 minutes, all facts, interruptible at any time.'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Start Button */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5, duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onStartPresenter}
|
||||||
|
className="group relative px-8 py-4 rounded-2xl bg-gradient-to-r from-indigo-600 to-purple-600
|
||||||
|
hover:from-indigo-500 hover:to-purple-500 transition-all duration-300
|
||||||
|
text-white font-semibold text-lg shadow-lg shadow-indigo-600/30
|
||||||
|
hover:shadow-xl hover:shadow-indigo-600/40 hover:scale-105"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-3">
|
||||||
|
{isPresenting ? (
|
||||||
|
<>
|
||||||
|
<Pause className="w-5 h-5" />
|
||||||
|
{isDE ? 'Praesentation laeuft...' : 'Presentation running...'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Play className="w-5 h-5" />
|
||||||
|
{isDE ? 'Praesentation starten' : 'Start Presentation'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Interaction hints */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8, duration: 0.5 }}
|
||||||
|
className="mt-10 flex flex-col md:flex-row gap-4 text-sm text-white/40"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<MessageCircle className="w-4 h-4" />
|
||||||
|
<span>{isDE ? 'Jederzeit Fragen im Chat stellen' : 'Ask questions in chat anytime'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="px-1.5 py-0.5 bg-white/10 rounded text-xs font-mono">P</span>
|
||||||
|
<span>{isDE ? 'Taste P: Presenter An/Aus' : 'Press P: Toggle Presenter'}</span>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { useState } from 'react'
|
|||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { Language, PitchMarket } from '@/lib/types'
|
import { Language, PitchMarket } from '@/lib/types'
|
||||||
import { t, formatEur } from '@/lib/i18n'
|
import { t, formatEur } from '@/lib/i18n'
|
||||||
import { ExternalLink, X, TrendingUp } from 'lucide-react'
|
import { ExternalLink, X, TrendingUp, Shield } from 'lucide-react'
|
||||||
import GradientText from '../ui/GradientText'
|
import GradientText from '../ui/GradientText'
|
||||||
import FadeInView from '../ui/FadeInView'
|
import FadeInView from '../ui/FadeInView'
|
||||||
import AnimatedCounter from '../ui/AnimatedCounter'
|
import AnimatedCounter from '../ui/AnimatedCounter'
|
||||||
@@ -60,6 +60,61 @@ const marketSources: Record<string, MarketSourceInfo[]> = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Pentesting / AppSec Market Data ──────────────────────────────────────────
|
||||||
|
|
||||||
|
type MarketView = 'compliance' | 'pentesting'
|
||||||
|
|
||||||
|
interface PentestMarketEntry {
|
||||||
|
segment: string
|
||||||
|
label: { de: string; en: string }
|
||||||
|
value_eur: number
|
||||||
|
growth_rate_pct: number
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PENTEST_MARKET: PentestMarketEntry[] = [
|
||||||
|
{ segment: 'TAM', label: { de: 'Total Addressable Market', en: 'Total Addressable Market' }, value_eur: 13_000_000_000, growth_rate_pct: 17, source: 'Gartner + MarketsAndMarkets 2025' },
|
||||||
|
{ segment: 'SAM', label: { de: 'Serviceable Addressable Market', en: 'Serviceable Addressable Market' }, value_eur: 1_600_000_000, growth_rate_pct: 22, source: 'DACH AST + Pentesting (Bottom-Up)' },
|
||||||
|
{ segment: 'SOM', label: { de: 'Serviceable Obtainable Market', en: 'Serviceable Obtainable Market' }, value_eur: 35_000_000, growth_rate_pct: 0, source: 'Year 5 Target (500 Kunden)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const pentestMarketSources: Record<string, MarketSourceInfo[]> = {
|
||||||
|
TAM: [
|
||||||
|
{
|
||||||
|
name: 'MarketsAndMarkets — Application Security Testing Market 2025',
|
||||||
|
url: 'https://www.marketsandmarkets.com/Market-Reports/application-security-testing-market-150735030.html',
|
||||||
|
date: '2025',
|
||||||
|
excerpt_de: 'Der globale AST-Markt (SAST, DAST, IAST, SCA) wird auf $8,5 Mrd. (2025) geschaetzt und soll bis 2030 auf $19,5 Mrd. wachsen (CAGR 18,2%). Hinzu kommt der Pentesting-Markt ($2,7 Mrd.) und der Compliance-Convergence-Anteil ($1,8 Mrd.). Gesamt-TAM fuer integriertes AppSec + Compliance: ~$13 Mrd.',
|
||||||
|
excerpt_en: 'The global AST market (SAST, DAST, IAST, SCA) is estimated at $8.5B (2025), projected to reach $19.5B by 2030 (CAGR 18.2%). Adding the pentesting market ($2.7B) and compliance convergence share ($1.8B), total TAM for integrated AppSec + compliance: ~$13B.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gartner — Magic Quadrant for Application Security Testing 2024',
|
||||||
|
url: 'https://www.gartner.com/reviews/market/application-security-testing',
|
||||||
|
date: '2024',
|
||||||
|
excerpt_de: 'Gartner bestaetigt den Trend zur Konvergenz von AppSec und Compliance. Fuehrende Anbieter (Snyk, Veracode, Checkmarx) erreichen zusammen >$850M Umsatz. Der Markt waechst mit 17-20% p.a., getrieben durch regulatorische Anforderungen (CRA, NIS2) und AI-getriebene Entwicklung.',
|
||||||
|
excerpt_en: 'Gartner confirms the AppSec-compliance convergence trend. Leading vendors (Snyk, Veracode, Checkmarx) generate >$850M combined revenue. The market grows at 17-20% p.a., driven by regulatory requirements (CRA, NIS2) and AI-driven development.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SAM: [
|
||||||
|
{
|
||||||
|
name: 'Bottom-Up: DACH AppSec + Manufacturing Pentesting',
|
||||||
|
url: 'https://www.bitkom.org/Marktdaten/ITK-Konjunktur/IT-Markt-Deutschland',
|
||||||
|
date: '2025-2026',
|
||||||
|
excerpt_de: 'DACH IT-Security-Markt: €8,2 Mrd. (Bitkom 2025). AppSec-Anteil: ~15% = €1,2 Mrd. Davon Pentesting/DAST/SAST fuer produzierende Industrie: ~€400M. CRA-Pflicht fuer Maschinenbauer erzeugt neue Nachfrage: geschaetzt +€200M bis 2028. SAM fuer integriertes AppSec + Compliance im DACH-Manufacturing: ~€1,6 Mrd.',
|
||||||
|
excerpt_en: 'DACH IT security market: €8.2B (Bitkom 2025). AppSec share: ~15% = €1.2B. Pentesting/DAST/SAST for manufacturing: ~€400M. CRA obligation for manufacturers creates new demand: est. +€200M by 2028. SAM for integrated AppSec + compliance in DACH manufacturing: ~€1.6B.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SOM: [
|
||||||
|
{
|
||||||
|
name: 'VDMA + Branchenbenchmarks — Pentesting SOM',
|
||||||
|
url: 'https://www.vdma.org/statistics',
|
||||||
|
date: '2025-2026',
|
||||||
|
excerpt_de: 'Zielmarkt: 5.000 DACH-Maschinenbauer mit Eigenentwicklung. Bei 10% Durchdringung (500 Unternehmen) und €70K/Jahr Blended ARPU (Compliance €18K + AppSec €52K) ergibt sich ein SOM von €35 Mio. in Year 5. Zum Vergleich: Pentera erreicht mit 400 MA $100M ARR bei 900 Kunden. Intruder (100 MA) erreicht $10M bei 2.500 Kunden.',
|
||||||
|
excerpt_en: 'Target market: 5,000 DACH manufacturers with in-house development. At 10% penetration (500 companies) and €70K/yr blended ARPU (compliance €18K + AppSec €52K), SOM is €35M in Year 5. For comparison: Pentera achieves $100M ARR with 400 employees and 900 customers. Intruder (100 employees) achieves $10M with 2,500 customers.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
const sizes = [280, 200, 130]
|
const sizes = [280, 200, 130]
|
||||||
const colors = ['border-indigo-500/30 bg-indigo-500/5', 'border-purple-500/30 bg-purple-500/5', 'border-blue-500/30 bg-blue-500/5']
|
const colors = ['border-indigo-500/30 bg-indigo-500/5', 'border-purple-500/30 bg-purple-500/5', 'border-blue-500/30 bg-blue-500/5']
|
||||||
const textColors = ['text-indigo-400', 'text-purple-400', 'text-blue-400']
|
const textColors = ['text-indigo-400', 'text-purple-400', 'text-blue-400']
|
||||||
@@ -69,14 +124,16 @@ function SourceModal({
|
|||||||
onClose,
|
onClose,
|
||||||
segment,
|
segment,
|
||||||
lang,
|
lang,
|
||||||
|
sources: sourcesMap,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
segment: string
|
segment: string
|
||||||
lang: Language
|
lang: Language
|
||||||
|
sources?: Record<string, MarketSourceInfo[]>
|
||||||
}) {
|
}) {
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
const sources = marketSources[segment] || []
|
const sources = (sourcesMap || marketSources)[segment] || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@@ -148,18 +205,47 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
const segments = [i.market.tam, i.market.sam, i.market.som]
|
const segments = [i.market.tam, i.market.sam, i.market.som]
|
||||||
const segmentKeys = ['TAM', 'SAM', 'SOM']
|
const segmentKeys = ['TAM', 'SAM', 'SOM']
|
||||||
const [activeModal, setActiveModal] = useState<string | null>(null)
|
const [activeModal, setActiveModal] = useState<string | null>(null)
|
||||||
|
const [marketView, setMarketView] = useState<MarketView>('compliance')
|
||||||
|
|
||||||
|
const activeSources = marketView === 'compliance' ? marketSources : pentestMarketSources
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FadeInView className="text-center mb-12">
|
<FadeInView className="text-center mb-6">
|
||||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||||
<GradientText>{i.market.title}</GradientText>
|
<GradientText>{i.market.title}</GradientText>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.market.subtitle}</p>
|
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.market.subtitle}</p>
|
||||||
</FadeInView>
|
</FadeInView>
|
||||||
|
|
||||||
|
{/* Market View Toggle */}
|
||||||
|
<FadeInView delay={0.1} className="flex justify-center gap-2 mb-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setMarketView('compliance')}
|
||||||
|
className={`px-4 py-1.5 rounded-full text-xs font-medium transition-all ${
|
||||||
|
marketView === 'compliance'
|
||||||
|
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||||
|
: 'bg-white/[0.04] text-white/40 border border-white/5 hover:bg-white/[0.08]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{lang === 'de' ? 'Compliance & Code-Security' : 'Compliance & Code Security'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMarketView('pentesting')}
|
||||||
|
className={`px-4 py-1.5 rounded-full text-xs font-medium transition-all flex items-center gap-1.5 ${
|
||||||
|
marketView === 'pentesting'
|
||||||
|
? 'bg-red-500/20 text-red-300 border border-red-500/30'
|
||||||
|
: 'bg-white/[0.04] text-white/40 border border-white/5 hover:bg-white/[0.08]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Shield className="w-3 h-3" />
|
||||||
|
{lang === 'de' ? 'Pentesting & AppSec' : 'Pentesting & AppSec'}
|
||||||
|
</button>
|
||||||
|
</FadeInView>
|
||||||
|
|
||||||
|
{/* Compliance Market */}
|
||||||
|
{marketView === 'compliance' && (
|
||||||
<div className="flex flex-col md:flex-row items-center justify-center gap-12">
|
<div className="flex flex-col md:flex-row items-center justify-center gap-12">
|
||||||
{/* Circles */}
|
|
||||||
<div className="relative flex items-center justify-center" style={{ width: 300, height: 300 }}>
|
<div className="relative flex items-center justify-center" style={{ width: 300, height: 300 }}>
|
||||||
{market.map((m, idx) => (
|
{market.map((m, idx) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -168,10 +254,7 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ delay: 0.3 + idx * 0.2, type: 'spring', stiffness: 200 }}
|
transition={{ delay: 0.3 + idx * 0.2, type: 'spring', stiffness: 200 }}
|
||||||
className={`absolute rounded-full border-2 ${colors[idx]} flex items-center justify-center`}
|
className={`absolute rounded-full border-2 ${colors[idx]} flex items-center justify-center`}
|
||||||
style={{
|
style={{ width: sizes[idx], height: sizes[idx] }}
|
||||||
width: sizes[idx],
|
|
||||||
height: sizes[idx],
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{idx === market.length - 1 && (
|
{idx === market.length - 1 && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -181,8 +264,6 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Labels */}
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{market.map((m, idx) => {
|
{market.map((m, idx) => {
|
||||||
const segKey = segmentKeys[idx] || m.market_segment
|
const segKey = segmentKeys[idx] || m.market_segment
|
||||||
@@ -205,26 +286,11 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold text-white">
|
<div className="text-2xl font-bold text-white">
|
||||||
{m.value_eur >= 1_000_000_000 ? (
|
{m.value_eur >= 1_000_000_000 ? (
|
||||||
<AnimatedCounter
|
<AnimatedCounter target={m.value_eur / 1_000_000_000} suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'} decimals={1} duration={1500} />
|
||||||
target={m.value_eur / 1_000_000_000}
|
|
||||||
suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'}
|
|
||||||
decimals={1}
|
|
||||||
duration={1500}
|
|
||||||
/>
|
|
||||||
) : m.value_eur >= 1_000_000 ? (
|
) : m.value_eur >= 1_000_000 ? (
|
||||||
<AnimatedCounter
|
<AnimatedCounter target={m.value_eur / 1_000_000} suffix={lang === 'de' ? ' Mio. EUR' : 'M EUR'} decimals={1} duration={1500} />
|
||||||
target={m.value_eur / 1_000_000}
|
|
||||||
suffix={lang === 'de' ? ' Mio. EUR' : 'M EUR'}
|
|
||||||
decimals={1}
|
|
||||||
duration={1500}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<AnimatedCounter
|
<AnimatedCounter target={m.value_eur / 1_000} suffix={'k EUR'} decimals={0} duration={1500} />
|
||||||
target={m.value_eur / 1_000}
|
|
||||||
suffix={'k EUR'}
|
|
||||||
decimals={0}
|
|
||||||
duration={1500}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 text-xs">
|
<div className="flex items-center gap-3 text-xs">
|
||||||
@@ -234,14 +300,11 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
{m.growth_rate_pct}% p.a.
|
{m.growth_rate_pct}% p.a.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-white/40">
|
<span className="text-white/40">{i.market.source}: {m.source}</span>
|
||||||
{i.market.source}: {m.source}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-indigo-400/60 group-hover:text-indigo-400 transition-colors mt-0.5">
|
<p className="text-[10px] text-indigo-400/60 group-hover:text-indigo-400 transition-colors mt-0.5">
|
||||||
{sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')}
|
{sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')}
|
||||||
{' · '}
|
{' · '}{lang === 'de' ? 'Klicken fuer Details' : 'Click for details'}
|
||||||
{lang === 'de' ? 'Klicken fuer Details' : 'Click for details'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,15 +313,93 @@ export default function MarketSlide({ lang, market }: MarketSlideProps) {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Source Modals */}
|
{/* Pentesting Market */}
|
||||||
|
{marketView === 'pentesting' && (
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-center gap-12">
|
||||||
|
<div className="relative flex items-center justify-center" style={{ width: 300, height: 300 }}>
|
||||||
|
{PENTEST_MARKET.map((pm, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={pm.segment}
|
||||||
|
initial={{ scale: 0, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ delay: 0.3 + idx * 0.2, type: 'spring', stiffness: 200 }}
|
||||||
|
className={`absolute rounded-full border-2 ${
|
||||||
|
idx === 0 ? 'border-red-500/30 bg-red-500/5' :
|
||||||
|
idx === 1 ? 'border-orange-500/30 bg-orange-500/5' :
|
||||||
|
'border-yellow-500/30 bg-yellow-500/5'
|
||||||
|
} flex items-center justify-center`}
|
||||||
|
style={{ width: sizes[idx], height: sizes[idx] }}
|
||||||
|
>
|
||||||
|
{idx === PENTEST_MARKET.length - 1 && (
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-xs font-mono text-yellow-400">{pm.segment}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{PENTEST_MARKET.map((pm, idx) => {
|
||||||
|
const ptColors = ['text-red-400', 'text-orange-400', 'text-yellow-400']
|
||||||
|
const sourceCount = pentestMarketSources[pm.segment]?.length || 0
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={pm.segment}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: 0.5 + idx * 0.15 }}
|
||||||
|
className="group cursor-pointer"
|
||||||
|
onClick={() => setActiveModal(pm.segment)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className={`w-3 h-3 rounded-full ${ptColors[idx]} bg-current`} />
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`text-sm font-bold ${ptColors[idx]}`}>{pm.segment}</span>
|
||||||
|
<span className="text-xs text-white/30">{pm.label[lang]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold text-white">
|
||||||
|
{pm.value_eur >= 1_000_000_000 ? (
|
||||||
|
<AnimatedCounter target={pm.value_eur / 1_000_000_000} suffix={lang === 'de' ? ' Mrd. EUR' : 'B EUR'} decimals={1} duration={1500} />
|
||||||
|
) : pm.value_eur >= 1_000_000 ? (
|
||||||
|
<AnimatedCounter target={pm.value_eur / 1_000_000} suffix={lang === 'de' ? ' Mio. EUR' : 'M EUR'} decimals={1} duration={1500} />
|
||||||
|
) : (
|
||||||
|
<AnimatedCounter target={pm.value_eur / 1_000} suffix={'k EUR'} decimals={0} duration={1500} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-xs">
|
||||||
|
{pm.growth_rate_pct > 0 && (
|
||||||
|
<span className="flex items-center gap-1 text-emerald-400">
|
||||||
|
<TrendingUp className="w-3 h-3" />
|
||||||
|
{pm.growth_rate_pct}% p.a.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-white/40">{i.market.source}: {pm.source}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] text-red-400/60 group-hover:text-red-400 transition-colors mt-0.5">
|
||||||
|
{sourceCount} {lang === 'de' ? (sourceCount === 1 ? 'Quelle' : 'Quellen') : (sourceCount === 1 ? 'source' : 'sources')}
|
||||||
|
{' · '}{lang === 'de' ? 'Klicken fuer Details' : 'Click for details'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Source Modals — both compliance and pentesting */}
|
||||||
{segmentKeys.map((seg) => (
|
{segmentKeys.map((seg) => (
|
||||||
<SourceModal
|
<SourceModal
|
||||||
key={seg}
|
key={`c-${seg}`}
|
||||||
isOpen={activeModal === seg}
|
isOpen={activeModal === seg}
|
||||||
onClose={() => setActiveModal(null)}
|
onClose={() => setActiveModal(null)}
|
||||||
segment={seg}
|
segment={seg}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
sources={activeSources}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface UseKeyboardProps {
|
|||||||
onFullscreen: () => void
|
onFullscreen: () => void
|
||||||
onLanguageToggle: () => void
|
onLanguageToggle: () => void
|
||||||
onMenuToggle: () => void
|
onMenuToggle: () => void
|
||||||
|
onPresenterToggle?: () => void
|
||||||
onGoToSlide: (index: number) => void
|
onGoToSlide: (index: number) => void
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ export function useKeyboard({
|
|||||||
onFullscreen,
|
onFullscreen,
|
||||||
onLanguageToggle,
|
onLanguageToggle,
|
||||||
onMenuToggle,
|
onMenuToggle,
|
||||||
|
onPresenterToggle,
|
||||||
onGoToSlide,
|
onGoToSlide,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
}: UseKeyboardProps) {
|
}: UseKeyboardProps) {
|
||||||
@@ -74,6 +76,11 @@ export function useKeyboard({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onMenuToggle()
|
onMenuToggle()
|
||||||
break
|
break
|
||||||
|
case 'p':
|
||||||
|
case 'P':
|
||||||
|
e.preventDefault()
|
||||||
|
onPresenterToggle?.()
|
||||||
|
break
|
||||||
case '1':
|
case '1':
|
||||||
case '2':
|
case '2':
|
||||||
case '3':
|
case '3':
|
||||||
@@ -88,7 +95,7 @@ export function useKeyboard({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[enabled, onNext, onPrev, onFirst, onLast, onOverview, onFullscreen, onLanguageToggle, onMenuToggle, onGoToSlide]
|
[enabled, onNext, onPrev, onFirst, onLast, onOverview, onFullscreen, onLanguageToggle, onMenuToggle, onPresenterToggle, onGoToSlide]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
254
pitch-deck/lib/hooks/usePresenterMode.ts
Normal file
254
pitch-deck/lib/hooks/usePresenterMode.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||||
|
import { Language, SlideId } from '../types'
|
||||||
|
import { PresenterState } from '../presenter/types'
|
||||||
|
import { PRESENTER_SCRIPT } from '../presenter/presenter-script'
|
||||||
|
import { SLIDE_ORDER } from './useSlideNavigation'
|
||||||
|
|
||||||
|
interface UsePresenterModeConfig {
|
||||||
|
goToSlide: (index: number) => void
|
||||||
|
currentSlide: number
|
||||||
|
totalSlides: number
|
||||||
|
language: Language
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsePresenterModeReturn {
|
||||||
|
state: PresenterState
|
||||||
|
currentParagraph: number
|
||||||
|
start: () => void
|
||||||
|
stop: () => void
|
||||||
|
pause: () => void
|
||||||
|
resume: () => void
|
||||||
|
skipSlide: () => void
|
||||||
|
toggle: () => void
|
||||||
|
displayText: string
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePresenterMode({
|
||||||
|
goToSlide,
|
||||||
|
currentSlide,
|
||||||
|
totalSlides,
|
||||||
|
language,
|
||||||
|
}: UsePresenterModeConfig): UsePresenterModeReturn {
|
||||||
|
const [state, setState] = useState<PresenterState>('idle')
|
||||||
|
const [currentParagraph, setCurrentParagraph] = useState(0)
|
||||||
|
const [displayText, setDisplayText] = useState('')
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const slideIndexRef = useRef(currentSlide)
|
||||||
|
const paragraphIndexRef = useRef(0)
|
||||||
|
const stateRef = useRef<PresenterState>('idle')
|
||||||
|
|
||||||
|
// Keep refs in sync
|
||||||
|
useEffect(() => {
|
||||||
|
slideIndexRef.current = currentSlide
|
||||||
|
}, [currentSlide])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateRef.current = state
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
const clearTimer = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
timerRef.current = null
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getScriptForIndex = useCallback((index: number) => {
|
||||||
|
const slideId = SLIDE_ORDER[index]
|
||||||
|
return PRESENTER_SCRIPT.find(s => s.slideId === slideId)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const showParagraph = useCallback((slideIdx: number, paraIdx: number) => {
|
||||||
|
const script = getScriptForIndex(slideIdx)
|
||||||
|
if (!script || paraIdx >= script.paragraphs.length) return null
|
||||||
|
|
||||||
|
const para = script.paragraphs[paraIdx]
|
||||||
|
const text = language === 'de' ? para.text_de : para.text_en
|
||||||
|
setDisplayText(text)
|
||||||
|
setCurrentParagraph(paraIdx)
|
||||||
|
paragraphIndexRef.current = paraIdx
|
||||||
|
return para
|
||||||
|
}, [language, getScriptForIndex])
|
||||||
|
|
||||||
|
const advancePresentation = useCallback(() => {
|
||||||
|
if (stateRef.current !== 'presenting') return
|
||||||
|
|
||||||
|
const slideIdx = slideIndexRef.current
|
||||||
|
const script = getScriptForIndex(slideIdx)
|
||||||
|
|
||||||
|
if (!script) {
|
||||||
|
// No script for this slide, advance to next
|
||||||
|
if (slideIdx < totalSlides - 1) {
|
||||||
|
goToSlide(slideIdx + 1)
|
||||||
|
paragraphIndexRef.current = 0
|
||||||
|
// Schedule next after slide transition
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), 2000)
|
||||||
|
} else {
|
||||||
|
setState('idle')
|
||||||
|
setDisplayText('')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPara = paragraphIndexRef.current + 1
|
||||||
|
|
||||||
|
if (nextPara < script.paragraphs.length) {
|
||||||
|
// Show next paragraph
|
||||||
|
const para = showParagraph(slideIdx, nextPara)
|
||||||
|
if (para) {
|
||||||
|
// Calculate display time: ~150ms per word + pause
|
||||||
|
const wordCount = (language === 'de' ? para.text_de : para.text_en).split(/\s+/).length
|
||||||
|
const readingTime = Math.max(wordCount * 150, 2000)
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), readingTime + para.pause_after)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// All paragraphs done for this slide
|
||||||
|
// Show transition hint briefly
|
||||||
|
if (script.transition_hint_de || script.transition_hint_en) {
|
||||||
|
const hint = language === 'de'
|
||||||
|
? (script.transition_hint_de || '')
|
||||||
|
: (script.transition_hint_en || '')
|
||||||
|
setDisplayText(hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to next slide
|
||||||
|
if (slideIdx < totalSlides - 1) {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
if (stateRef.current !== 'presenting') return
|
||||||
|
goToSlide(slideIdx + 1)
|
||||||
|
paragraphIndexRef.current = -1 // Will be incremented to 0
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
if (stateRef.current !== 'presenting') return
|
||||||
|
const nextScript = getScriptForIndex(slideIdx + 1)
|
||||||
|
if (nextScript && nextScript.paragraphs.length > 0) {
|
||||||
|
const para = showParagraph(slideIdx + 1, 0)
|
||||||
|
if (para) {
|
||||||
|
const wordCount = (language === 'de' ? para.text_de : para.text_en).split(/\s+/).length
|
||||||
|
const readingTime = Math.max(wordCount * 150, 2000)
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), readingTime + para.pause_after)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
advancePresentation()
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
// Last slide — done
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setState('idle')
|
||||||
|
setDisplayText('')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [language, totalSlides, goToSlide, getScriptForIndex, showParagraph])
|
||||||
|
|
||||||
|
const start = useCallback(() => {
|
||||||
|
clearTimer()
|
||||||
|
setState('presenting')
|
||||||
|
|
||||||
|
const slideIdx = slideIndexRef.current
|
||||||
|
const script = getScriptForIndex(slideIdx)
|
||||||
|
|
||||||
|
if (script && script.paragraphs.length > 0) {
|
||||||
|
const para = showParagraph(slideIdx, 0)
|
||||||
|
if (para) {
|
||||||
|
const wordCount = (language === 'de' ? para.text_de : para.text_en).split(/\s+/).length
|
||||||
|
const readingTime = Math.max(wordCount * 150, 2000)
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), readingTime + para.pause_after)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No script, advance immediately
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), 1000)
|
||||||
|
}
|
||||||
|
}, [clearTimer, language, getScriptForIndex, showParagraph, advancePresentation])
|
||||||
|
|
||||||
|
const stop = useCallback(() => {
|
||||||
|
clearTimer()
|
||||||
|
setState('idle')
|
||||||
|
setDisplayText('')
|
||||||
|
setCurrentParagraph(0)
|
||||||
|
paragraphIndexRef.current = 0
|
||||||
|
}, [clearTimer])
|
||||||
|
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
clearTimer()
|
||||||
|
setState('paused')
|
||||||
|
}, [clearTimer])
|
||||||
|
|
||||||
|
const resume = useCallback(() => {
|
||||||
|
setState('resuming')
|
||||||
|
// Brief pause before continuing
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
setState('presenting')
|
||||||
|
// Continue from where we left off
|
||||||
|
advancePresentation()
|
||||||
|
}, 2000)
|
||||||
|
}, [advancePresentation])
|
||||||
|
|
||||||
|
const skipSlide = useCallback(() => {
|
||||||
|
clearTimer()
|
||||||
|
const nextIdx = slideIndexRef.current + 1
|
||||||
|
if (nextIdx < totalSlides) {
|
||||||
|
goToSlide(nextIdx)
|
||||||
|
paragraphIndexRef.current = -1
|
||||||
|
|
||||||
|
if (stateRef.current === 'presenting') {
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
const script = getScriptForIndex(nextIdx)
|
||||||
|
if (script && script.paragraphs.length > 0) {
|
||||||
|
const para = showParagraph(nextIdx, 0)
|
||||||
|
if (para) {
|
||||||
|
const wordCount = (language === 'de' ? para.text_de : para.text_en).split(/\s+/).length
|
||||||
|
const readingTime = Math.max(wordCount * 150, 2000)
|
||||||
|
timerRef.current = setTimeout(() => advancePresentation(), readingTime + para.pause_after)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [clearTimer, totalSlides, goToSlide, language, getScriptForIndex, showParagraph, advancePresentation])
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
if (stateRef.current === 'idle') {
|
||||||
|
start()
|
||||||
|
} else {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}, [start, stop])
|
||||||
|
|
||||||
|
// Calculate overall progress
|
||||||
|
const progress = (() => {
|
||||||
|
if (state === 'idle') return 0
|
||||||
|
const totalScripts = PRESENTER_SCRIPT.length
|
||||||
|
const currentScriptIdx = PRESENTER_SCRIPT.findIndex(s => s.slideId === SLIDE_ORDER[currentSlide])
|
||||||
|
if (currentScriptIdx < 0) return (currentSlide / totalSlides) * 100
|
||||||
|
|
||||||
|
const script = PRESENTER_SCRIPT[currentScriptIdx]
|
||||||
|
const slideProgress = script.paragraphs.length > 0
|
||||||
|
? currentParagraph / script.paragraphs.length
|
||||||
|
: 0
|
||||||
|
return ((currentScriptIdx + slideProgress) / totalScripts) * 100
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => clearTimer()
|
||||||
|
}, [clearTimer])
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
currentParagraph,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
pause,
|
||||||
|
resume,
|
||||||
|
skipSlide,
|
||||||
|
toggle,
|
||||||
|
displayText,
|
||||||
|
progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import { SlideId } from '../types'
|
import { SlideId } from '../types'
|
||||||
|
|
||||||
const SLIDE_ORDER: SlideId[] = [
|
export const SLIDE_ORDER: SlideId[] = [
|
||||||
|
'intro-presenter',
|
||||||
'cover',
|
'cover',
|
||||||
'problem',
|
'problem',
|
||||||
'solution',
|
'solution',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const translations = {
|
|||||||
language: 'Sprache',
|
language: 'Sprache',
|
||||||
},
|
},
|
||||||
slideNames: [
|
slideNames: [
|
||||||
|
'Intro',
|
||||||
'Cover',
|
'Cover',
|
||||||
'Das Problem',
|
'Das Problem',
|
||||||
'Die Loesung',
|
'Die Loesung',
|
||||||
@@ -25,6 +26,8 @@ const translations = {
|
|||||||
'Anhang: Architektur',
|
'Anhang: Architektur',
|
||||||
'Anhang: Go-to-Market',
|
'Anhang: Go-to-Market',
|
||||||
'Anhang: Regulatorik',
|
'Anhang: Regulatorik',
|
||||||
|
'Anhang: Engineering',
|
||||||
|
'Anhang: KI-Pipeline',
|
||||||
],
|
],
|
||||||
cover: {
|
cover: {
|
||||||
tagline: 'Compliance & Code-Security fuer den Maschinenbau',
|
tagline: 'Compliance & Code-Security fuer den Maschinenbau',
|
||||||
@@ -228,6 +231,7 @@ const translations = {
|
|||||||
language: 'Language',
|
language: 'Language',
|
||||||
},
|
},
|
||||||
slideNames: [
|
slideNames: [
|
||||||
|
'Intro',
|
||||||
'Cover',
|
'Cover',
|
||||||
'The Problem',
|
'The Problem',
|
||||||
'The Solution',
|
'The Solution',
|
||||||
@@ -245,6 +249,8 @@ const translations = {
|
|||||||
'Appendix: Architecture',
|
'Appendix: Architecture',
|
||||||
'Appendix: Go-to-Market',
|
'Appendix: Go-to-Market',
|
||||||
'Appendix: Regulatory',
|
'Appendix: Regulatory',
|
||||||
|
'Appendix: Engineering',
|
||||||
|
'Appendix: AI Pipeline',
|
||||||
],
|
],
|
||||||
cover: {
|
cover: {
|
||||||
tagline: 'Compliance & Code Security for Machine Manufacturers',
|
tagline: 'Compliance & Code Security for Machine Manufacturers',
|
||||||
|
|||||||
72
pitch-deck/lib/presenter/faq-matcher.ts
Normal file
72
pitch-deck/lib/presenter/faq-matcher.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Language } from '../types'
|
||||||
|
import { FAQEntry } from './types'
|
||||||
|
import { PRESENTER_FAQ } from './presenter-faq'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a user query against pre-cached FAQ entries.
|
||||||
|
* Returns the best match if score exceeds threshold, or null for LLM fallback.
|
||||||
|
*/
|
||||||
|
export function matchFAQ(query: string, lang: Language): FAQEntry | null {
|
||||||
|
const normalized = query.toLowerCase().trim()
|
||||||
|
const queryWords = normalized.split(/\s+/)
|
||||||
|
|
||||||
|
let bestMatch: FAQEntry | null = null
|
||||||
|
let bestScore = 0
|
||||||
|
|
||||||
|
for (const entry of PRESENTER_FAQ) {
|
||||||
|
let score = 0
|
||||||
|
|
||||||
|
// Check keyword matches
|
||||||
|
for (const keyword of entry.keywords) {
|
||||||
|
const kwLower = keyword.toLowerCase()
|
||||||
|
if (kwLower.includes(' ')) {
|
||||||
|
// Multi-word keyword: check if phrase appears in query
|
||||||
|
if (normalized.includes(kwLower)) {
|
||||||
|
score += 3 * entry.priority / 10
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single keyword: check word-level match
|
||||||
|
if (queryWords.some(w => w === kwLower || w.startsWith(kwLower) || kwLower.startsWith(w))) {
|
||||||
|
score += 1
|
||||||
|
}
|
||||||
|
// Also check if keyword appears anywhere in query (partial match)
|
||||||
|
if (normalized.includes(kwLower)) {
|
||||||
|
score += 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if query matches the question text closely
|
||||||
|
const questionText = lang === 'de' ? entry.question_de : entry.question_en
|
||||||
|
const questionWords = questionText.toLowerCase().split(/\s+/)
|
||||||
|
const overlapCount = queryWords.filter(w =>
|
||||||
|
w.length > 2 && questionWords.some(qw => qw.includes(w) || w.includes(qw))
|
||||||
|
).length
|
||||||
|
if (overlapCount >= 2) {
|
||||||
|
score += overlapCount * 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight by priority
|
||||||
|
score *= (entry.priority / 10)
|
||||||
|
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score
|
||||||
|
bestMatch = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Threshold: need meaningful match to avoid false positives
|
||||||
|
// Require at least 2 keyword hits or strong phrase match
|
||||||
|
if (bestScore < 1.5) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get FAQ answer text in the requested language
|
||||||
|
*/
|
||||||
|
export function getFAQAnswer(entry: FAQEntry, lang: Language): string {
|
||||||
|
return lang === 'de' ? entry.answer_de : entry.answer_en
|
||||||
|
}
|
||||||
300
pitch-deck/lib/presenter/presenter-faq.ts
Normal file
300
pitch-deck/lib/presenter/presenter-faq.ts
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import { FAQEntry } from './types'
|
||||||
|
|
||||||
|
export const PRESENTER_FAQ: FAQEntry[] = [
|
||||||
|
// === PRODUCT ===
|
||||||
|
{
|
||||||
|
id: 'product-what',
|
||||||
|
keywords: ['was', 'macht', 'breakpilot', 'what', 'does', 'product', 'produkt'],
|
||||||
|
question_de: 'Was macht BreakPilot?',
|
||||||
|
question_en: 'What does BreakPilot do?',
|
||||||
|
answer_de: 'BreakPilot ComplAI ist eine KI-gesteuerte Compliance- und Code-Security-Plattform fuer Maschinenbauer. Eine Self-Hosted Hardware-Appliance (Mac Mini/Studio) scannt Code und erstellt Compliance-Dokumente. 57 Module decken DSGVO, AI Act, CRA und NIS2 ab — mit 2.274 indexierten Rechtstexten.',
|
||||||
|
answer_en: 'BreakPilot ComplAI is an AI-powered compliance and code security platform for machine manufacturers. A self-hosted hardware appliance (Mac Mini/Studio) scans code and creates compliance documents. 57 modules cover GDPR, AI Act, CRA and NIS2 — with 2,274 indexed legal texts.',
|
||||||
|
goto_slide: 'solution',
|
||||||
|
priority: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'product-modules',
|
||||||
|
keywords: ['module', 'modules', 'funktionen', 'features', 'umfang', 'scope', 'wieviele', 'how many'],
|
||||||
|
question_de: 'Welche Module hat die Plattform?',
|
||||||
|
question_en: 'What modules does the platform have?',
|
||||||
|
answer_de: '57 Compliance-Module: DSGVO (VVT, DSFA, TOM, DSR, Loeschfristen), AI Act (Risikoklassifizierung, Dokumentation), CRA (Code-Security, SBOM), NIS2 (Incident Response, Notfallplan), plus Vendor Compliance, Audit-Management, Policy Generator und mehr.',
|
||||||
|
answer_en: '57 compliance modules: GDPR (RoPA, DPIA, TOMs, DSR, deletion deadlines), AI Act (risk classification, documentation), CRA (code security, SBOM), NIS2 (incident response, emergency plans), plus vendor compliance, audit management, policy generator and more.',
|
||||||
|
goto_slide: 'solution',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'product-dsgvo-vs-aiact',
|
||||||
|
keywords: ['dsgvo', 'gdpr', 'ai act', 'unterschied', 'difference', 'versus', 'vs'],
|
||||||
|
question_de: 'Was ist der Unterschied zwischen DSGVO und AI Act Compliance?',
|
||||||
|
question_en: 'What is the difference between GDPR and AI Act compliance?',
|
||||||
|
answer_de: 'DSGVO schuetzt personenbezogene Daten (Verarbeitungsverzeichnis, Loeschfristen, Betroffenenrechte). Der AI Act reguliert KI-Systeme (Risikoklassifizierung, Transparenz, Human Oversight). Maschinenbauer mit KI in ihren Produkten brauchen beides — und genau das liefern wir.',
|
||||||
|
answer_en: 'GDPR protects personal data (records of processing, deletion deadlines, data subject rights). The AI Act regulates AI systems (risk classification, transparency, human oversight). Machine manufacturers with AI in their products need both — and that is exactly what we deliver.',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === TECHNOLOGY ===
|
||||||
|
{
|
||||||
|
id: 'tech-llm',
|
||||||
|
keywords: ['llm', 'modell', 'model', 'ki', 'ai', 'kuenstliche intelligenz', 'artificial intelligence', 'welches', 'which'],
|
||||||
|
question_de: 'Welches KI-Modell nutzt ihr?',
|
||||||
|
question_en: 'Which AI model do you use?',
|
||||||
|
answer_de: 'Wir setzen auf eine proprietaere KI-Engine mit verschiedenen Modellgroessen: 32B Parameter lokal auf Mac Mini, 40B auf Mac Studio, und ein BSI-zertifiziertes 1000B Cloud-LLM fuer komplexe Aufgaben. Die lokalen Modelle machen die Vorarbeit, die Cloud implementiert Fixes.',
|
||||||
|
answer_en: 'We use a proprietary AI engine with different model sizes: 32B parameters locally on Mac Mini, 40B on Mac Studio, and a BSI-certified 1000B cloud LLM for complex tasks. Local models do the preprocessing, the cloud implements fixes.',
|
||||||
|
goto_slide: 'product',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-opensource',
|
||||||
|
keywords: ['open source', 'opensource', 'quellcode', 'source code', 'lizenz', 'license'],
|
||||||
|
question_de: 'Ist die Plattform Open Source?',
|
||||||
|
question_en: 'Is the platform open source?',
|
||||||
|
answer_de: 'Die Plattform selbst ist proprietaer, nutzt aber ausschliesslich Open-Source-Dependencies mit kommerziell nutzbaren Lizenzen (MIT, Apache-2.0, BSD). Wir verwenden keine GPL/AGPL-abhaengigkeiten. Die Hardware laeuft auf Apple Silicon.',
|
||||||
|
answer_en: 'The platform itself is proprietary but uses exclusively open source dependencies with commercially usable licenses (MIT, Apache-2.0, BSD). We use no GPL/AGPL dependencies. The hardware runs on Apple Silicon.',
|
||||||
|
priority: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-security',
|
||||||
|
keywords: ['sicherheit', 'security', 'datenschutz', 'privacy', 'verschluesselung', 'encryption', 'hosting'],
|
||||||
|
question_de: 'Wie sicher sind die Daten?',
|
||||||
|
question_en: 'How secure is the data?',
|
||||||
|
answer_de: 'Maximale Datensouveraenitaet: Die Hardware steht im Serverraum des Kunden. Alle Daten bleiben on-premise. Nur anonymisierte Anfragen gehen an die BSI-zertifizierte Cloud. TLS 1.3 fuer alle Verbindungen, Vault fuer Secrets, Enterprise-Grade Verschluesselung.',
|
||||||
|
answer_en: 'Maximum data sovereignty: The hardware sits in the customer server room. All data stays on-premise. Only anonymized queries go to the BSI-certified cloud. TLS 1.3 for all connections, Vault for secrets, enterprise-grade encryption.',
|
||||||
|
goto_slide: 'annex-architecture',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-selfhosted',
|
||||||
|
keywords: ['self-hosted', 'selfhosted', 'self hosted', 'on-premise', 'onpremise', 'lokal', 'local', 'warum', 'why', 'mac'],
|
||||||
|
question_de: 'Warum Self-Hosted auf Apple Hardware?',
|
||||||
|
question_en: 'Why self-hosted on Apple hardware?',
|
||||||
|
answer_de: 'Drei Gruende: 1) Datensouveraenitaet — Kundendaten verlassen nie das Unternehmen. 2) Apple Silicon bietet das beste Preis-Leistungs-Verhaeltnis fuer lokale LLM-Inferenz. 3) Hardware-Moat — die Appliance schafft einen natuerlichen Lock-in und differentiert uns vom Wettbewerb.',
|
||||||
|
answer_en: 'Three reasons: 1) Data sovereignty — customer data never leaves the company. 2) Apple Silicon offers the best price-performance ratio for local LLM inference. 3) Hardware moat — the appliance creates a natural lock-in and differentiates us from competition.',
|
||||||
|
goto_slide: 'solution',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-code-scanning',
|
||||||
|
keywords: ['code', 'scan', 'scanning', 'firmware', 'devsecops', 'trivy', 'semgrep', 'schwachstellen', 'vulnerability'],
|
||||||
|
question_de: 'Wie funktioniert das Code-Scanning?',
|
||||||
|
question_en: 'How does code scanning work?',
|
||||||
|
answer_de: 'Integrierte DevSecOps-Tools (Trivy, Semgrep, Gitleaks) scannen automatisch Git-Repos und Firmware bei jedem Commit. Die lokale KI priorisiert Findings, das Cloud-LLM implementiert Fixes und schreibt Risikoanalysen fuer den CRA.',
|
||||||
|
answer_en: 'Integrated DevSecOps tools (Trivy, Semgrep, Gitleaks) automatically scan Git repos and firmware on every commit. The local AI prioritizes findings, the cloud LLM implements fixes and writes risk assessments for the CRA.',
|
||||||
|
goto_slide: 'how-it-works',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === MARKET ===
|
||||||
|
{
|
||||||
|
id: 'market-tam',
|
||||||
|
keywords: ['tam', 'sam', 'som', 'markt', 'market', 'marktgroesse', 'market size', 'adressierbar'],
|
||||||
|
question_de: 'Wie gross ist der Markt?',
|
||||||
|
question_en: 'How big is the market?',
|
||||||
|
answer_de: 'TAM: 8,7 Mrd. EUR (globaler RegTech-Markt, +23% p.a.). SAM: 1,2 Mrd. EUR (DACH-Maschinenbauer mit Softwareentwicklung). SOM: 7,2 Mio. EUR (500 Kunden x 14.400 EUR/Jahr). Der CRA allein betrifft ueber 30.000 Hersteller in der EU.',
|
||||||
|
answer_en: 'TAM: EUR 8.7B (global RegTech market, +23% p.a.). SAM: EUR 1.2B (DACH machine manufacturers with software development). SOM: EUR 7.2M (500 customers x EUR 14,400/year). The CRA alone affects over 30,000 manufacturers in the EU.',
|
||||||
|
goto_slide: 'market',
|
||||||
|
priority: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'market-target',
|
||||||
|
keywords: ['zielgruppe', 'target', 'kunden', 'customers', 'wer', 'who', 'maschinenbau', 'machine', 'vdma'],
|
||||||
|
question_de: 'Wer ist die Zielgruppe?',
|
||||||
|
question_en: 'Who is the target audience?',
|
||||||
|
answer_de: 'Primaer: Maschinen- und Anlagenbauer (VDMA ~3.600 Mitglieder in DE, ~5.000 DACH) die eigene Software und Firmware entwickeln. Diese Unternehmen muessen CRA, DSGVO, AI Act und NIS2 einhalten — haben aber keine spezialisierten Compliance-Tools fuer ihren Code.',
|
||||||
|
answer_en: 'Primary: Machine and plant manufacturers (VDMA ~3,600 members in DE, ~5,000 DACH) who develop their own software and firmware. These companies must comply with CRA, GDPR, AI Act and NIS2 — but lack specialized compliance tools for their code.',
|
||||||
|
goto_slide: 'market',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === COMPETITION ===
|
||||||
|
{
|
||||||
|
id: 'competition-diff',
|
||||||
|
keywords: ['wettbewerb', 'competition', 'konkurrenz', 'unterschied', 'differenzierung', 'differentiation', 'proliance', 'dataguard', 'heydata', 'vanta'],
|
||||||
|
question_de: 'Was unterscheidet euch vom Wettbewerb?',
|
||||||
|
question_en: 'What differentiates you from the competition?',
|
||||||
|
answer_de: 'Drei entscheidende Unterschiede: 1) Code-Security — wir scannen Firmware und Software, das kann kein anderer Compliance-Anbieter. 2) Self-Hosted KI — maximale Datensouveraenitaet. 3) Produkt-Compliance — wir machen nicht nur das Unternehmen, sondern auch die Produkte compliant (CRA).',
|
||||||
|
answer_en: 'Three decisive differences: 1) Code security — we scan firmware and software, no other compliance provider can do that. 2) Self-hosted AI — maximum data sovereignty. 3) Product compliance — we make not only the company, but also the products compliant (CRA).',
|
||||||
|
goto_slide: 'competition',
|
||||||
|
priority: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'competition-proliance',
|
||||||
|
keywords: ['proliance', 'dataguard', 'heydata', 'vergleich', 'comparison', 'versus'],
|
||||||
|
question_de: 'Warum koennen Proliance und DataGuard das nicht?',
|
||||||
|
question_en: 'Why can\'t Proliance and DataGuard do this?',
|
||||||
|
answer_de: 'Proliance, DataGuard und heyData fokussieren auf organisatorische DSGVO-Compliance — Verarbeitungsverzeichnisse, Datenschutzerklaerungen, Schulungen. Keiner bietet Code-Scanning, Firmware-Analyse oder CRA-Compliance. Sie machen das Unternehmen compliant, aber nicht die Produkte.',
|
||||||
|
answer_en: 'Proliance, DataGuard and heyData focus on organizational GDPR compliance — records of processing, privacy policies, training. None offer code scanning, firmware analysis or CRA compliance. They make the company compliant, but not the products.',
|
||||||
|
goto_slide: 'competition',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === BUSINESS MODEL ===
|
||||||
|
{
|
||||||
|
id: 'biz-pricing',
|
||||||
|
keywords: ['preis', 'price', 'pricing', 'kosten', 'cost', 'kostet', 'costs', 'wie viel', 'how much', 'subscription'],
|
||||||
|
question_de: 'Was kostet BreakPilot?',
|
||||||
|
question_en: 'What does BreakPilot cost?',
|
||||||
|
answer_de: 'Drei Tiers: ComplAI Starter (Mac Mini) ab 1.200 EUR/Monat, Professional (Mac Studio) ab 2.400 EUR/Monat, Enterprise mit Cloud-LLM ab 3.600 EUR/Monat. Die Hardware wird einmalig bereitgestellt, die Software laeuft als monatliche Subscription.',
|
||||||
|
answer_en: 'Three tiers: ComplAI Starter (Mac Mini) from EUR 1,200/month, Professional (Mac Studio) from EUR 2,400/month, Enterprise with cloud LLM from EUR 3,600/month. Hardware is provided once, software runs as monthly subscription.',
|
||||||
|
goto_slide: 'product',
|
||||||
|
priority: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'biz-revenue',
|
||||||
|
keywords: ['umsatz', 'revenue', 'arr', 'mrr', 'recurring', 'einnahmen', 'income'],
|
||||||
|
question_de: 'Wie sieht der Umsatzplan aus?',
|
||||||
|
question_en: 'What does the revenue plan look like?',
|
||||||
|
answer_de: 'Von 36k EUR (2026) auf 8,4 Mio EUR (2030). Rein recurring — monatliche Subscriptions. 380 Kunden bei 5,5 Mio EUR ARR in 2030. Break-Even voraussichtlich Ende 2028.',
|
||||||
|
answer_en: 'From EUR 36k (2026) to EUR 8.4M (2030). Purely recurring — monthly subscriptions. 380 customers at EUR 5.5M ARR in 2030. Break-even expected end of 2028.',
|
||||||
|
goto_slide: 'financials',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'biz-unit-economics',
|
||||||
|
keywords: ['unit economics', 'marge', 'margin', 'ltv', 'cac', 'amortisation', 'amortization'],
|
||||||
|
question_de: 'Wie sind die Unit Economics?',
|
||||||
|
question_en: 'What are the unit economics?',
|
||||||
|
answer_de: 'Bruttomarge ueber 70%. Hardware-Kosten amortisieren sich in wenigen Monaten. LTV/CAC Ratio verbessert sich auf ueber 5x bis 2029. Die AI-First Architektur haelt die operativen Kosten pro Kunde extrem niedrig.',
|
||||||
|
answer_en: 'Gross margin above 70%. Hardware costs amortize in a few months. LTV/CAC ratio improves to over 5x by 2029. The AI-first architecture keeps operational costs per customer extremely low.',
|
||||||
|
goto_slide: 'business-model',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'biz-scalability',
|
||||||
|
keywords: ['skalierung', 'scalability', 'skalierbar', 'scalable', 'wachstum', 'growth', 'personal', 'headcount'],
|
||||||
|
question_de: 'Wie skaliert das Geschaeftsmodell?',
|
||||||
|
question_en: 'How does the business model scale?',
|
||||||
|
answer_de: 'AI-First bedeutet: 10x Kunden erfordert nicht 10x Personal. Die KI automatisiert Compliance-Analyse und Code-Scanning. Das Team waechst von 2 auf 18 Personen bei 380 Kunden. Die Hardware-Appliance ist Plug-and-Play — kein aufwendiges Onboarding noetig.',
|
||||||
|
answer_en: 'AI-first means: 10x customers does not require 10x headcount. The AI automates compliance analysis and code scanning. The team grows from 2 to 18 people at 380 customers. The hardware appliance is plug-and-play — no extensive onboarding needed.',
|
||||||
|
goto_slide: 'financials',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === TEAM ===
|
||||||
|
{
|
||||||
|
id: 'team-founders',
|
||||||
|
keywords: ['team', 'gruender', 'founders', 'wer', 'who', 'erfahrung', 'experience', 'hintergrund', 'background'],
|
||||||
|
question_de: 'Wer sind die Gruender?',
|
||||||
|
question_en: 'Who are the founders?',
|
||||||
|
answer_de: 'Unser Gruenderteam vereint tiefe Domain-Expertise: Compliance-Wissen aus der Praxis, Software-Architektur fuer skalierbare Systeme, und KI-Erfahrung mit Large Language Models. Details finden Sie auf der Team-Slide.',
|
||||||
|
answer_en: 'Our founding team combines deep domain expertise: Compliance knowledge from practice, software architecture for scalable systems, and AI experience with large language models. Details on the team slide.',
|
||||||
|
goto_slide: 'team',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === INVESTMENT ===
|
||||||
|
{
|
||||||
|
id: 'invest-amount',
|
||||||
|
keywords: ['investment', 'investition', 'funding', 'finanzierung', 'wie viel', 'how much', 'kapital', 'capital', 'runde', 'round'],
|
||||||
|
question_de: 'Wie viel Kapital sucht ihr?',
|
||||||
|
question_en: 'How much capital are you seeking?',
|
||||||
|
answer_de: 'Wir suchen eine Pre-Seed Finanzierung. Das genaue Volumen und die Konditionen besprechen wir gerne im Detail. Das Kapital fliesst in Engineering, Vertrieb, Hardware-Bestand und regulatorische Reserve.',
|
||||||
|
answer_en: 'We are seeking pre-seed funding. We are happy to discuss the exact volume and terms in detail. Capital goes into engineering, sales, hardware inventory and regulatory reserve.',
|
||||||
|
goto_slide: 'the-ask',
|
||||||
|
priority: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invest-use-of-funds',
|
||||||
|
keywords: ['use of funds', 'wofuer', 'what for', 'verwendung', 'allocation', 'mittelverwendung'],
|
||||||
|
question_de: 'Wofuer wird das Kapital verwendet?',
|
||||||
|
question_en: 'What will the capital be used for?',
|
||||||
|
answer_de: 'Vier Bereiche: 1) Engineering — Produktreife und weitere Module. 2) Vertrieb — erste Pilotkunden im VDMA-Netzwerk. 3) Hardware — Mac Minis/Studios fuer schnelle Auslieferung. 4) Reserve — regulatorische Anforderungen und Working Capital.',
|
||||||
|
answer_en: 'Four areas: 1) Engineering — product maturity and additional modules. 2) Sales — first pilot customers in VDMA network. 3) Hardware — Mac Minis/Studios for fast delivery. 4) Reserve — regulatory requirements and working capital.',
|
||||||
|
goto_slide: 'the-ask',
|
||||||
|
priority: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invest-runway',
|
||||||
|
keywords: ['runway', 'burn', 'burn rate', 'reicht', 'lasts', 'monate', 'months', 'cashflow'],
|
||||||
|
question_de: 'Wie lang reicht das Kapital?',
|
||||||
|
question_en: 'How long does the capital last?',
|
||||||
|
answer_de: 'Die Pre-Seed Runde finanziert uns bis zur naechsten Finanzierungsrunde. Dank AI-First Architektur und Self-Hosted Ansatz ist unsere Burn Rate signifikant niedriger als bei Cloud-basierten SaaS-Startups.',
|
||||||
|
answer_en: 'The pre-seed round funds us until the next financing round. Thanks to AI-first architecture and self-hosted approach, our burn rate is significantly lower than cloud-based SaaS startups.',
|
||||||
|
goto_slide: 'financials',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invest-valuation',
|
||||||
|
keywords: ['bewertung', 'valuation', 'konditionen', 'terms', 'instrument', 'safe', 'convertible'],
|
||||||
|
question_de: 'Was ist die Bewertung?',
|
||||||
|
question_en: 'What is the valuation?',
|
||||||
|
answer_de: 'Die genaue Bewertung und das Instrument besprechen wir gerne im persoenlichen Gespraech. Wir sind offen fuer marktgerechte Strukturen, die fuer beide Seiten attraktiv sind.',
|
||||||
|
answer_en: 'We are happy to discuss the exact valuation and instrument in a personal meeting. We are open to market-standard structures that are attractive for both sides.',
|
||||||
|
goto_slide: 'the-ask',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === REGULATORY ===
|
||||||
|
{
|
||||||
|
id: 'reg-cra',
|
||||||
|
keywords: ['cra', 'cyber resilience', 'cyber resilience act', 'firmware', 'produktsicherheit', 'product security'],
|
||||||
|
question_de: 'Was ist der Cyber Resilience Act?',
|
||||||
|
question_en: 'What is the Cyber Resilience Act?',
|
||||||
|
answer_de: 'Der CRA verpflichtet Hersteller, Software in ihren Produkten abzusichern — ueber den gesamten Lebenszyklus. Fuer Maschinenbauer mit Firmware und embedded Software bedeutet das: Vulnerability Management, SBOM, Incident Reporting. Genau das automatisiert BreakPilot.',
|
||||||
|
answer_en: 'The CRA obligates manufacturers to secure software in their products — throughout the entire lifecycle. For machine manufacturers with firmware and embedded software this means: vulnerability management, SBOM, incident reporting. That is exactly what BreakPilot automates.',
|
||||||
|
goto_slide: 'annex-regulatory',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reg-nis2',
|
||||||
|
keywords: ['nis2', 'nis 2', 'cybersecurity', 'kritische infrastruktur', 'critical infrastructure'],
|
||||||
|
question_de: 'Was bedeutet NIS2 fuer Maschinenbauer?',
|
||||||
|
question_en: 'What does NIS2 mean for machine manufacturers?',
|
||||||
|
answer_de: 'NIS2 erweitert die Cybersecurity-Pflichten auf den Maschinenbau. Unternehmen muessen Risikomanagement, Incident Response und Supply-Chain-Security implementieren. BreakPilot bietet Module fuer alle drei Bereiche.',
|
||||||
|
answer_en: 'NIS2 extends cybersecurity obligations to machine manufacturing. Companies must implement risk management, incident response and supply chain security. BreakPilot offers modules for all three areas.',
|
||||||
|
goto_slide: 'annex-regulatory',
|
||||||
|
priority: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === GTM ===
|
||||||
|
{
|
||||||
|
id: 'gtm-strategy',
|
||||||
|
keywords: ['gtm', 'go-to-market', 'go to market', 'vertrieb', 'sales', 'strategie', 'strategy', 'akquise', 'acquisition'],
|
||||||
|
question_de: 'Wie gewinnt ihr Kunden?',
|
||||||
|
question_en: 'How do you acquire customers?',
|
||||||
|
answer_de: 'Drei Kanaele: 1) VDMA-Netzwerk — direkter Zugang zu 3.600+ Maschinenbauern. 2) Partnerschaften mit Systemhaeusern und IT-Dienstleistern. 3) Content Marketing ueber Compliance-Webinare und CRA-Awareness. Start mit 5 Pilotkunden, dann skalieren.',
|
||||||
|
answer_en: 'Three channels: 1) VDMA network — direct access to 3,600+ machine manufacturers. 2) Partnerships with system integrators and IT service providers. 3) Content marketing through compliance webinars and CRA awareness. Start with 5 pilot customers, then scale.',
|
||||||
|
goto_slide: 'annex-gtm',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gtm-timeline',
|
||||||
|
keywords: ['timeline', 'zeitplan', 'schedule', 'wann', 'when', 'roadmap', 'meilensteine', 'milestones'],
|
||||||
|
question_de: 'Wann kommen die ersten Kunden?',
|
||||||
|
question_en: 'When will the first customers come?',
|
||||||
|
answer_de: 'Die Plattform ist produktionsreif. Nach der Pre-Seed Runde starten wir sofort mit Pilotprojekten. Ziel: 5 zahlende Pilotkunden innerhalb von 6 Monaten, 20 Kunden nach 12 Monaten.',
|
||||||
|
answer_en: 'The platform is production-ready. After the pre-seed round we start pilot projects immediately. Target: 5 paying pilot customers within 6 months, 20 customers after 12 months.',
|
||||||
|
goto_slide: 'traction',
|
||||||
|
priority: 7,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === PENTESTING CONVERGENCE ===
|
||||||
|
{
|
||||||
|
id: 'pentest',
|
||||||
|
keywords: ['pentesting', 'penetrationstest', 'penetration test', 'security testing', 'pentests'],
|
||||||
|
question_de: 'Plant ihr auch Pentesting-Services?',
|
||||||
|
question_en: 'Do you plan pentesting services?',
|
||||||
|
answer_de: 'Ja, Pentesting ist eine natuerliche Erweiterung. Unsere Code-Security-Tools identifizieren bereits Schwachstellen — der naechste Schritt ist automatisiertes Pentesting. Der Pentesting-Markt konvergiert mit Compliance — wir sind an der Schnittstelle positioniert.',
|
||||||
|
answer_en: 'Yes, pentesting is a natural extension. Our code security tools already identify vulnerabilities — the next step is automated pentesting. The pentesting market converges with compliance — we are positioned at the intersection.',
|
||||||
|
priority: 6,
|
||||||
|
},
|
||||||
|
|
||||||
|
// === MISC ===
|
||||||
|
{
|
||||||
|
id: 'misc-demo',
|
||||||
|
keywords: ['demo', 'test', 'testen', 'try', 'ausprobieren', 'live', 'showcase'],
|
||||||
|
question_de: 'Kann ich eine Demo sehen?',
|
||||||
|
question_en: 'Can I see a demo?',
|
||||||
|
answer_de: 'Sehr gerne! Wir zeigen Ihnen die Plattform live — inklusive Code-Scanning, Compliance-Module und KI-Analyse. Kontaktieren Sie uns fuer einen Termin.',
|
||||||
|
answer_en: 'Absolutely! We will show you the platform live — including code scanning, compliance modules and AI analysis. Contact us for an appointment.',
|
||||||
|
priority: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'misc-contact',
|
||||||
|
keywords: ['kontakt', 'contact', 'email', 'mail', 'erreichen', 'reach', 'termin', 'meeting', 'appointment'],
|
||||||
|
question_de: 'Wie kann ich euch kontaktieren?',
|
||||||
|
question_en: 'How can I contact you?',
|
||||||
|
answer_de: 'Schreiben Sie uns eine E-Mail oder vereinbaren Sie direkt einen Termin ueber unsere Website. Wir freuen uns auf das Gespraech!',
|
||||||
|
answer_en: 'Send us an email or schedule a meeting directly through our website. We look forward to the conversation!',
|
||||||
|
priority: 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
463
pitch-deck/lib/presenter/presenter-script.ts
Normal file
463
pitch-deck/lib/presenter/presenter-script.ts
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
import { SlideScript } from './types'
|
||||||
|
|
||||||
|
export const PRESENTER_SCRIPT: SlideScript[] = [
|
||||||
|
// 0 — intro-presenter (45s)
|
||||||
|
{
|
||||||
|
slideId: 'intro-presenter',
|
||||||
|
duration: 45,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Willkommen bei BreakPilot ComplAI — Compliance und Code-Security fuer den Maschinenbau.',
|
||||||
|
text_en: 'Welcome to BreakPilot ComplAI — compliance and code security for machine manufacturing.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Ich bin Ihr KI-Praesentator und fuehre Sie durch unser Pitch Deck. Die Praesentation dauert etwa 15 Minuten.',
|
||||||
|
text_en: 'I am your AI presenter and will guide you through our pitch deck. The presentation takes about 15 minutes.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Sie koennen jederzeit Fragen stellen — nutzen Sie einfach den Chat. Ich pausiere automatisch und antworte sofort.',
|
||||||
|
text_en: 'You can ask questions at any time — just use the chat. I will pause automatically and respond immediately.',
|
||||||
|
pause_after: 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Lassen Sie uns beginnen.',
|
||||||
|
transition_hint_en: 'Let us begin.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 1 — cover (20s)
|
||||||
|
{
|
||||||
|
slideId: 'cover',
|
||||||
|
duration: 20,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'BreakPilot ComplAI — Compliance und Code-Security auf Autopilot. Pre-Seed Runde, Q4 2026.',
|
||||||
|
text_en: 'BreakPilot ComplAI — compliance and code security on autopilot. Pre-seed round, Q4 2026.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Schauen wir uns zunaechst das Problem an.',
|
||||||
|
transition_hint_en: 'Let us first look at the problem.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 2 — problem (60s)
|
||||||
|
{
|
||||||
|
slideId: 'problem',
|
||||||
|
duration: 60,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Maschinenbauer entwickeln immer mehr eigene Software — Firmware, Steuerungen, Predictive Maintenance. Aber wer sichert Compliance und Code-Sicherheit?',
|
||||||
|
text_en: 'Machine manufacturers develop more and more of their own software — firmware, controllers, predictive maintenance. But who ensures compliance and code security?',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Erstens: DSGVO. 4,1 Milliarden Euro Bussgelder seit 2018. Maschinenbauer verarbeiten Kundendaten, Telemetrie und Wartungsprotokolle — oft ohne DSGVO-Prozesse.',
|
||||||
|
text_en: 'First: GDPR. EUR 4.1 billion in fines since 2018. Machine manufacturers process customer data, telemetry and maintenance logs — often without GDPR processes.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Zweitens: Der AI Act. Ab August 2025 muessen Maschinen mit KI-Komponenten klassifiziert und dokumentiert werden.',
|
||||||
|
text_en: 'Second: The AI Act. From August 2025, machines with AI components must be classified and documented.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Drittens: CRA und NIS2. Der Cyber Resilience Act verpflichtet ueber 30.000 Hersteller, Software in ihren Produkten abzusichern. Das betrifft den gesamten Maschinenbau.',
|
||||||
|
text_en: 'Third: CRA and NIS2. The Cyber Resilience Act obligates over 30,000 manufacturers to secure software in their products. This affects all of machine manufacturing.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Und genau dafuer haben wir eine Loesung.',
|
||||||
|
transition_hint_en: 'And that is exactly what we have a solution for.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3 — solution (75s)
|
||||||
|
{
|
||||||
|
slideId: 'solution',
|
||||||
|
duration: 75,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unsere Loesung: ComplAI — Compliance und Code-Security auf Autopilot. Drei Saeulen machen uns einzigartig.',
|
||||||
|
text_en: 'Our solution: ComplAI — compliance and code security on autopilot. Three pillars make us unique.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Erste Saeule: Self-Hosted Vorarbeit. Ein Mac Mini oder Mac Studio im Serverraum des Kunden scannt Code, analysiert Repositories und erstellt Compliance-Dokumente. Kein einziges Byte verlaesst das Unternehmen.',
|
||||||
|
text_en: 'First pillar: Self-hosted preprocessing. A Mac Mini or Mac Studio in the customer server room scans code, analyzes repositories and creates compliance documents. Not a single byte leaves the company.',
|
||||||
|
pause_after: 2500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Zweite Saeule: Code-Security und DevSecOps. Wir scannen Firmware und Software mit integrierten Tools wie Trivy, Semgrep und Gitleaks. Das 1000-Milliarden-Parameter Cloud-LLM implementiert Fixes und schreibt Risikoanalysen.',
|
||||||
|
text_en: 'Second pillar: Code security and DevSecOps. We scan firmware and software with integrated tools like Trivy, Semgrep, and Gitleaks. The 1000-billion-parameter cloud LLM implements fixes and writes risk assessments.',
|
||||||
|
pause_after: 2500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Dritte Saeule: Die Compliance-KI mit 57 Modulen. DSGVO, AI Act, CRA, NIS2, Hinweisgeberschutzgesetz — 19 Regularien, 2.274 indexierte Rechtstexte. Macht Ihr Unternehmen UND Ihre Produkte compliant.',
|
||||||
|
text_en: 'Third pillar: The compliance AI with 57 modules. GDPR, AI Act, CRA, NIS2, Whistleblower Protection Act — 19 regulations, 2,274 indexed legal texts. Makes your company AND your products compliant.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Schauen wir uns die Produkte im Detail an.',
|
||||||
|
transition_hint_en: 'Let us look at the products in detail.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 4 — product (60s)
|
||||||
|
{
|
||||||
|
slideId: 'product',
|
||||||
|
duration: 60,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Wir bieten drei Produkt-Tiers fuer jede Unternehmensgroesse an.',
|
||||||
|
text_en: 'We offer three product tiers for every company size.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'ComplAI Starter auf dem Mac Mini fuer 1.200 Euro pro Monat. Ein 32-Milliarden-Parameter Modell — perfekt fuer KMU, die DSGVO-Compliance automatisieren wollen.',
|
||||||
|
text_en: 'ComplAI Starter on Mac Mini for EUR 1,200 per month. A 32-billion-parameter model — perfect for SMEs looking to automate GDPR compliance.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'ComplAI Professional auf dem Mac Studio fuer 2.400 Euro pro Monat mit einem 40-Milliarden-Parameter Modell. Inklusive Code-Security-Scanning und erweiterte Risikoanalyse.',
|
||||||
|
text_en: 'ComplAI Professional on Mac Studio for EUR 2,400 per month with a 40-billion-parameter model. Including code security scanning and extended risk assessment.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Und ComplAI Enterprise — unser Flaggschiff. Self-Hosted plus BSI-zertifiziertes 1000-Milliarden-Parameter Cloud-LLM. Fuer Unternehmen, die maximale Compliance und Code-Security benoetigen.',
|
||||||
|
text_en: 'And ComplAI Enterprise — our flagship. Self-hosted plus BSI-certified 1000-billion-parameter cloud LLM. For companies needing maximum compliance and code security.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Wie funktioniert das Ganze in der Praxis?',
|
||||||
|
transition_hint_en: 'How does this work in practice?',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 5 — how-it-works (50s)
|
||||||
|
{
|
||||||
|
slideId: 'how-it-works',
|
||||||
|
duration: 50,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'In vier einfachen Schritten zu Compliance und Code-Security.',
|
||||||
|
text_en: 'Compliance and code security in four simple steps.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Schritt 1: Hardware aufstellen. Mac Mini oder Mac Studio im Serverraum anschliessen — Plug and Play, scannt ab Tag 1.',
|
||||||
|
text_en: 'Step 1: Set up hardware. Connect Mac Mini or Mac Studio in the server room — plug and play, scans from day one.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Schritt 2: Code-Repos verbinden. Git-Repos, CI/CD Pipelines und Firmware-Projekte anbinden.',
|
||||||
|
text_en: 'Step 2: Connect code repos. Link Git repos, CI/CD pipelines and firmware projects.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Schritt 3: Automatisierung. Laufende Code-Analyse und Risikoanalysen bei jeder Aenderung. Bei kritischen Fixes schaltet sich das Cloud-LLM zu.',
|
||||||
|
text_en: 'Step 3: Automation. Continuous code analysis and risk assessments on every change. For critical fixes, the cloud LLM steps in.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Schritt 4: Audit bestehen. Vollstaendige Dokumentation fuer DSGVO, AI Act, CRA und NIS2 auf Knopfdruck.',
|
||||||
|
text_en: 'Step 4: Pass audits. Complete documentation for GDPR, AI Act, CRA and NIS2 at the push of a button.',
|
||||||
|
pause_after: 1000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Jetzt zur Marktchance.',
|
||||||
|
transition_hint_en: 'Now to the market opportunity.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 6 — market (60s)
|
||||||
|
{
|
||||||
|
slideId: 'market',
|
||||||
|
duration: 60,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Der Markt fuer Compliance und Code-Security im Maschinenbau ist enorm.',
|
||||||
|
text_en: 'The market for compliance and code security in machine manufacturing is enormous.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Unser Total Addressable Market liegt bei 8,7 Milliarden Euro — der globale Markt fuer Regulatory Technology waechst mit 23 Prozent pro Jahr.',
|
||||||
|
text_en: 'Our Total Addressable Market is EUR 8.7 billion — the global regulatory technology market grows at 23 percent per year.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Der Serviceable Addressable Market in DACH: 1,2 Milliarden Euro. Das sind ueber 5.000 Maschinenbauer mit eigener Softwareentwicklung.',
|
||||||
|
text_en: 'The Serviceable Addressable Market in DACH: EUR 1.2 billion. That is over 5,000 machine manufacturers with in-house software development.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Unser Serviceable Obtainable Market: 7,2 Millionen Euro. 500 DACH-Maschinenbauer mal 14.400 Euro pro Jahr. Das ist unser realistisches Ziel fuer die naechsten 5 Jahre.',
|
||||||
|
text_en: 'Our Serviceable Obtainable Market: EUR 7.2 million. 500 DACH machine manufacturers times EUR 14,400 per year. That is our realistic target for the next 5 years.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Wie verdienen wir Geld?',
|
||||||
|
transition_hint_en: 'How do we make money?',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 7 — business-model (45s)
|
||||||
|
{
|
||||||
|
slideId: 'business-model',
|
||||||
|
duration: 45,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unser Geschaeftsmodell: Recurring Revenue mit Hardware-Moat.',
|
||||||
|
text_en: 'Our business model: Recurring revenue with hardware moat.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Die Hardware-Appliance im Serverraum des Kunden schafft einen natuerlichen Lock-in. Monatliche Subscriptions von 1.200 bis 3.600 Euro garantieren planbare Einnahmen.',
|
||||||
|
text_en: 'The hardware appliance in the customer server room creates a natural lock-in. Monthly subscriptions from EUR 1,200 to 3,600 ensure predictable revenue.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Die Unit Economics sind ueberzeugend: Hardware-Kosten amortisieren sich in wenigen Monaten, und die Bruttomarge liegt bei ueber 70 Prozent.',
|
||||||
|
text_en: 'The unit economics are compelling: hardware costs amortize in a few months, and gross margin exceeds 70 percent.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Was haben wir bisher erreicht?',
|
||||||
|
transition_hint_en: 'What have we achieved so far?',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 8 — traction (45s)
|
||||||
|
{
|
||||||
|
slideId: 'traction',
|
||||||
|
duration: 45,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unsere bisherige Traction und Meilensteine.',
|
||||||
|
text_en: 'Our traction and milestones so far.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: '761.000 Zeilen Code geschrieben. 45 Container in Produktion. 57 Compliance-Module implementiert. 2.274 Rechtstexte indexiert und durchsuchbar.',
|
||||||
|
text_en: '761,000 lines of code written. 45 containers in production. 57 compliance modules implemented. 2,274 legal texts indexed and searchable.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Die komplette Plattform ist funktionsfaehig — wir sind bereit fuer die ersten zahlenden Kunden.',
|
||||||
|
text_en: 'The complete platform is functional — we are ready for our first paying customers.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Wie stehen wir im Vergleich zum Wettbewerb?',
|
||||||
|
transition_hint_en: 'How do we compare to the competition?',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 9 — competition (60s)
|
||||||
|
{
|
||||||
|
slideId: 'competition',
|
||||||
|
duration: 60,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Der Wettbewerb: 44 Features, 9 USPs — kein Anbieter kombiniert DSGVO, Code-Security und Self-Hosted KI.',
|
||||||
|
text_en: 'The competition: 44 features, 9 USPs — no provider combines GDPR, code security and self-hosted AI.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Proliance, DataGuard und heyData bieten organisatorische Compliance — aber keiner scannt Code, keiner analysiert Firmware, keiner bietet Self-Hosted KI.',
|
||||||
|
text_en: 'Proliance, DataGuard and heyData offer organizational compliance — but none scan code, none analyze firmware, none offer self-hosted AI.',
|
||||||
|
pause_after: 2500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Vanta und Drata kommen aus dem US-Markt mit SOC2-Fokus. Sie verstehen weder CRA noch die spezifischen Anforderungen des deutschen Maschinenbaus.',
|
||||||
|
text_en: 'Vanta and Drata come from the US market with SOC2 focus. They understand neither CRA nor the specific requirements of German machine manufacturing.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Unser Alleinstellungsmerkmal: Wir machen nicht nur das Unternehmen compliant, sondern auch die Produkte. Das ist der entscheidende Unterschied.',
|
||||||
|
text_en: 'Our unique selling proposition: We make not only the company compliant, but also the products. That is the decisive difference.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Lernen Sie unser Team kennen.',
|
||||||
|
transition_hint_en: 'Meet our team.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 10 — team (30s)
|
||||||
|
{
|
||||||
|
slideId: 'team',
|
||||||
|
duration: 30,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unser Gruenderteam vereint tiefe Domain-Expertise in Compliance, Software-Architektur und KI.',
|
||||||
|
text_en: 'Our founding team combines deep domain expertise in compliance, software architecture and AI.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Wir kennen die Schmerzen der Branche aus erster Hand und haben die technische Kompetenz, sie zu loesen.',
|
||||||
|
text_en: 'We know the pain points of the industry firsthand and have the technical competence to solve them.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Schauen wir uns die Finanzprognosen an.',
|
||||||
|
transition_hint_en: 'Let us look at the financial projections.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 11 — financials (45s)
|
||||||
|
{
|
||||||
|
slideId: 'financials',
|
||||||
|
duration: 45,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unsere Finanzprognose basiert auf einer AI-First Kostenstruktur — das heisst: 10x Kunden bedeutet nicht 10x Personal.',
|
||||||
|
text_en: 'Our financial projection is based on an AI-first cost structure — meaning: 10x customers does not mean 10x headcount.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Von 36.000 Euro Umsatz in 2026 auf 8,4 Millionen Euro in 2030. Das Team waechst dabei nur von 2 auf 18 Personen. 380 Kunden bei 5,5 Millionen Euro ARR.',
|
||||||
|
text_en: 'From EUR 36,000 revenue in 2026 to EUR 8.4 million in 2030. The team grows from just 2 to 18 people. 380 customers at EUR 5.5 million ARR.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Break-Even erreichen wir voraussichtlich Ende 2028. Die Burn Rate bleibt dank Self-Hosted Architektur und Apple-Silicon Effizienz niedrig.',
|
||||||
|
text_en: 'We expect to reach break-even by end of 2028. The burn rate stays low thanks to self-hosted architecture and Apple Silicon efficiency.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Und damit kommen wir zum Ask.',
|
||||||
|
transition_hint_en: 'And that brings us to the ask.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 12 — the-ask (45s)
|
||||||
|
{
|
||||||
|
slideId: 'the-ask',
|
||||||
|
duration: 45,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Wir suchen eine Pre-Seed Finanzierung fuer den Go-to-Market.',
|
||||||
|
text_en: 'We are seeking pre-seed funding for go-to-market.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Das Investment fliesst in vier Bereiche: Engineering fuer die Produktreife, Vertrieb fuer die ersten Pilotkunden, Hardware-Bestand fuer schnelle Auslieferung, und eine Reserve fuer regulatorische Anforderungen.',
|
||||||
|
text_en: 'The investment flows into four areas: Engineering for product maturity, sales for first pilot customers, hardware inventory for fast delivery, and a reserve for regulatory requirements.',
|
||||||
|
pause_after: 2500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Mit diesem Kapital erreichen wir die ersten 20 zahlenden Kunden und beweisen Product-Market-Fit im deutschen Maschinenbau.',
|
||||||
|
text_en: 'With this capital we reach our first 20 paying customers and prove product-market fit in German machine manufacturing.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Haben Sie Fragen? Unser KI-Agent steht bereit.',
|
||||||
|
transition_hint_en: 'Have questions? Our AI agent is ready.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 13 — ai-qa (30s)
|
||||||
|
{
|
||||||
|
slideId: 'ai-qa',
|
||||||
|
duration: 30,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Auf dieser Slide koennen Sie direkt mit unserem KI-Agent interagieren. Stellen Sie Ihre Investorenfragen — der Agent antwortet mit Echtdaten aus unserer Datenbank.',
|
||||||
|
text_en: 'On this slide you can interact directly with our AI agent. Ask your investor questions — the agent responds with real data from our database.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Nutzen Sie den Chat rechts unten oder die vorgeschlagenen Fragen.',
|
||||||
|
text_en: 'Use the chat in the bottom right or the suggested questions.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transition_hint_de: 'Im Anhang finden Sie weitere Details.',
|
||||||
|
transition_hint_en: 'You will find further details in the appendix.',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 14 — annex-assumptions (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-assumptions',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Im Anhang: Unsere Annahmen und Sensitivitaetsanalyse. Drei Szenarien — konservativ, base case und optimistisch — fuer robuste Planung.',
|
||||||
|
text_en: 'In the appendix: Our assumptions and sensitivity analysis. Three scenarios — conservative, base case and optimistic — for robust planning.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text_de: 'Alle Finanzprognosen basieren auf validierten Marktdaten und realistischen Wachstumsannahmen.',
|
||||||
|
text_en: 'All financial projections are based on validated market data and realistic growth assumptions.',
|
||||||
|
pause_after: 1500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 15 — annex-architecture (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-architecture',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Die technische Architektur: Self-Hosted KI-Stack fuer maximale Datensouveraenitaet. Alle Daten bleiben on-premise, nur anonymisierte Anfragen gehen an die BSI-zertifizierte Cloud.',
|
||||||
|
text_en: 'The technical architecture: Self-hosted AI stack for maximum data sovereignty. All data stays on-premise, only anonymized queries go to the BSI-certified cloud.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 16 — annex-gtm (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-gtm',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Unsere Go-to-Market Strategie: Vom Pilotprojekt zum skalierbaren Vertrieb. Wir starten mit VDMA-Mitgliedern und skalieren ueber Partnerschaften mit Systemhaeusern.',
|
||||||
|
text_en: 'Our go-to-market strategy: From pilot project to scalable sales. We start with VDMA members and scale through partnerships with system integrators.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 17 — annex-regulatory (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-regulatory',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Die vier Saeulen der EU-Compliance fuer Maschinenbauer: DSGVO, AI Act, Cyber Resilience Act und NIS2. Alle vier Regularien zusammen erzeugen einen massiven Compliance-Druck — und genau hier setzen wir an.',
|
||||||
|
text_en: 'The four pillars of EU compliance for machine manufacturers: GDPR, AI Act, Cyber Resilience Act and NIS2. All four regulations together create massive compliance pressure — and that is exactly where we come in.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 18 — annex-engineering (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-engineering',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Engineering Deep Dive: 761.000 Zeilen Code, 45 Container, 100 Prozent Self-Hosted. Unsere Plattform ist produktionsreif und skalierbar.',
|
||||||
|
text_en: 'Engineering deep dive: 761,000 lines of code, 45 containers, 100 percent self-hosted. Our platform is production-ready and scalable.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// 19 — annex-aipipeline (35s)
|
||||||
|
{
|
||||||
|
slideId: 'annex-aipipeline',
|
||||||
|
duration: 35,
|
||||||
|
paragraphs: [
|
||||||
|
{
|
||||||
|
text_de: 'Die KI-Pipeline im Detail: RAG, Multi-Agent-System, Document Intelligence und Quality Assurance. Vielen Dank fuer Ihre Aufmerksamkeit — ich stehe fuer weitere Fragen bereit.',
|
||||||
|
text_en: 'The AI pipeline in detail: RAG, multi-agent system, document intelligence and quality assurance. Thank you for your attention — I am available for further questions.',
|
||||||
|
pause_after: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getScriptForSlide(slideId: string): SlideScript | undefined {
|
||||||
|
return PRESENTER_SCRIPT.find(s => s.slideId === slideId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScriptByIndex(index: number): SlideScript | undefined {
|
||||||
|
return PRESENTER_SCRIPT[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTotalDuration(): number {
|
||||||
|
return PRESENTER_SCRIPT.reduce((sum, s) => sum + s.duration, 0)
|
||||||
|
}
|
||||||
20
pitch-deck/lib/presenter/tts-client.ts
Normal file
20
pitch-deck/lib/presenter/tts-client.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Language } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTS Stub — prepared for future Piper TTS integration via compliance-tts-service (:8095)
|
||||||
|
* POST /synthesize { text, voice: 'de_DE-thorsten-high' }
|
||||||
|
* Returns audio URL from MinIO
|
||||||
|
*/
|
||||||
|
export async function synthesizeSpeech(text: string, lang: Language): Promise<string | null> {
|
||||||
|
// TODO: Connect to compliance-tts-service
|
||||||
|
// const voice = lang === 'de' ? 'de_DE-thorsten-high' : 'en_US-lessac-high'
|
||||||
|
// const res = await fetch('http://compliance-tts-service:8095/synthesize', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify({ text, voice }),
|
||||||
|
// })
|
||||||
|
// if (!res.ok) return null
|
||||||
|
// const data = await res.json()
|
||||||
|
// return data.audio_url
|
||||||
|
return null
|
||||||
|
}
|
||||||
40
pitch-deck/lib/presenter/types.ts
Normal file
40
pitch-deck/lib/presenter/types.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Language, SlideId } from '../types'
|
||||||
|
|
||||||
|
export type PresenterState = 'idle' | 'presenting' | 'paused' | 'answering' | 'resuming'
|
||||||
|
|
||||||
|
export interface PresenterConfig {
|
||||||
|
autoAdvance: boolean
|
||||||
|
speechRate: number
|
||||||
|
pauseBetweenSlides: number
|
||||||
|
language: Language
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlideScript {
|
||||||
|
slideId: SlideId
|
||||||
|
duration: number
|
||||||
|
paragraphs: {
|
||||||
|
text_de: string
|
||||||
|
text_en: string
|
||||||
|
pause_after: number
|
||||||
|
}[]
|
||||||
|
transition_hint_de?: string
|
||||||
|
transition_hint_en?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FAQEntry {
|
||||||
|
id: string
|
||||||
|
keywords: string[]
|
||||||
|
question_de: string
|
||||||
|
question_en: string
|
||||||
|
answer_de: string
|
||||||
|
answer_en: string
|
||||||
|
goto_slide?: SlideId
|
||||||
|
priority: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PRESENTER_CONFIG: PresenterConfig = {
|
||||||
|
autoAdvance: true,
|
||||||
|
speechRate: 1.0,
|
||||||
|
pauseBetweenSlides: 2000,
|
||||||
|
language: 'de',
|
||||||
|
}
|
||||||
@@ -201,6 +201,7 @@ export interface ChatMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SlideId =
|
export type SlideId =
|
||||||
|
| 'intro-presenter'
|
||||||
| 'cover'
|
| 'cover'
|
||||||
| 'problem'
|
| 'problem'
|
||||||
| 'solution'
|
| 'solution'
|
||||||
|
|||||||
Reference in New Issue
Block a user