From b46436634192f833d3cf5084e60a6b22b678244e Mon Sep 17 00:00:00 2001 From: BreakPilot Dev Date: Sat, 14 Feb 2026 21:20:02 +0100 Subject: [PATCH] feat: Add staged funding model, financial compute engine, annex slides and UI enhancements Restructure financial plan from single 200k SAFE to realistic staged funding (25k Stammkapital, 25k Angel, 200k Wandeldarlehen, 1M Series A = 1.25M total). Add 60-month compute engine with CAPEX/OPEX accounting, cash constraints, hardware financing (30% upfront / 70% leasing), and revenue-based hiring caps. Rebuild TheAskSlide with 4-event funding timeline, update i18n (DE/EN), chat agent core messages, and add 15 new annex/technology slides with supporting UI components (KPICard, RunwayGauge, WaterfallChart, etc.). Co-Authored-By: Claude Opus 4.6 --- pitch-deck/app/api/chat/route.ts | 88 +++- .../api/financial-model/assumptions/route.ts | 30 ++ .../app/api/financial-model/compute/route.ts | 388 ++++++++++++++++ .../results/[scenarioId]/route.ts | 28 ++ pitch-deck/app/api/financial-model/route.ts | 73 +++ pitch-deck/components/ChatFAB.tsx | 420 ++++++++++++++++++ pitch-deck/components/NavigationFAB.tsx | 64 +-- pitch-deck/components/PitchDeck.tsx | 56 ++- pitch-deck/components/SlideOverview.tsx | 48 +- .../components/slides/AnnexAIStackSlide.tsx | 200 +++++++++ .../components/slides/AnnexAgentArchSlide.tsx | 147 ++++++ .../components/slides/AnnexAgentRAGSlide.tsx | 145 ++++++ .../slides/AnnexAgentWorkflowSlide.tsx | 162 +++++++ .../components/slides/AnnexDevOpsSlide.tsx | 146 ++++++ .../components/slides/AnnexInfraSlide.tsx | 97 ++++ .../components/slides/AnnexRAGSlide.tsx | 204 +++++++++ .../slides/AnnexRoadmap2027Slide.tsx | 142 ++++++ .../slides/AnnexRoadmap2028Slide.tsx | 142 ++++++ .../components/slides/AnnexSecuritySlide.tsx | 130 ++++++ .../slides/AnnexUSPComparisonSlide.tsx | 181 ++++++++ .../components/slides/AnnexUSPMoatSlide.tsx | 127 ++++++ .../slides/AnnexUSPOverviewSlide.tsx | 129 ++++++ .../components/slides/AppendixSlide.tsx | 68 +++ .../components/slides/CompetitionSlide.tsx | 3 +- pitch-deck/components/slides/CoverSlide.tsx | 11 +- .../components/slides/FinancialsSlide.tsx | 352 ++++++++++++--- .../components/slides/SolutionSlide.tsx | 5 +- .../components/slides/TechnologySlide.tsx | 125 ++++++ pitch-deck/components/slides/TheAskSlide.tsx | 143 ++++-- .../components/ui/AnnualCashflowChart.tsx | 205 +++++++++ pitch-deck/components/ui/AnnualPLTable.tsx | 232 ++++++++++ pitch-deck/components/ui/BrandName.tsx | 19 + pitch-deck/components/ui/FeatureMatrix.tsx | 3 +- pitch-deck/components/ui/FinancialChart.tsx | 192 ++++++-- pitch-deck/components/ui/FinancialSliders.tsx | 178 +++++--- pitch-deck/components/ui/KPICard.tsx | 59 +++ pitch-deck/components/ui/RunwayGauge.tsx | 133 ++++++ pitch-deck/components/ui/ScenarioSwitcher.tsx | 63 +++ .../components/ui/UnitEconomicsCards.tsx | 93 ++++ pitch-deck/components/ui/WaterfallChart.tsx | 85 ++++ pitch-deck/lib/hooks/useFinancialModel.ts | 109 +++++ pitch-deck/lib/hooks/useSlideNavigation.ts | 15 + pitch-deck/lib/i18n.ts | 124 +++++- pitch-deck/lib/types.ts | 94 ++++ 44 files changed, 5196 insertions(+), 262 deletions(-) create mode 100644 pitch-deck/app/api/financial-model/assumptions/route.ts create mode 100644 pitch-deck/app/api/financial-model/compute/route.ts create mode 100644 pitch-deck/app/api/financial-model/results/[scenarioId]/route.ts create mode 100644 pitch-deck/app/api/financial-model/route.ts create mode 100644 pitch-deck/components/ChatFAB.tsx create mode 100644 pitch-deck/components/slides/AnnexAIStackSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexAgentArchSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexAgentRAGSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexDevOpsSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexInfraSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexRAGSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx create mode 100644 pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx create mode 100644 pitch-deck/components/slides/AnnexSecuritySlide.tsx create mode 100644 pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexUSPMoatSlide.tsx create mode 100644 pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx create mode 100644 pitch-deck/components/slides/AppendixSlide.tsx create mode 100644 pitch-deck/components/slides/TechnologySlide.tsx create mode 100644 pitch-deck/components/ui/AnnualCashflowChart.tsx create mode 100644 pitch-deck/components/ui/AnnualPLTable.tsx create mode 100644 pitch-deck/components/ui/BrandName.tsx create mode 100644 pitch-deck/components/ui/KPICard.tsx create mode 100644 pitch-deck/components/ui/RunwayGauge.tsx create mode 100644 pitch-deck/components/ui/ScenarioSwitcher.tsx create mode 100644 pitch-deck/components/ui/UnitEconomicsCards.tsx create mode 100644 pitch-deck/components/ui/WaterfallChart.tsx create mode 100644 pitch-deck/lib/hooks/useFinancialModel.ts diff --git a/pitch-deck/app/api/chat/route.ts b/pitch-deck/app/api/chat/route.ts index 8b25558..56a20fc 100644 --- a/pitch-deck/app/api/chat/route.ts +++ b/pitch-deck/app/api/chat/route.ts @@ -21,8 +21,10 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen. 1. AI-First: "Alles was durch KI loesbar ist, wird durch KI geloest. Kein klassischer Support, kein grosses Sales-Team." 2. Skalierbarkeit: "10x Kunden ≠ 10x Personal. Die KI skaliert mit." 3. Hardware-Differenzierung: "Datensouveraenitaet durch Self-Hosting auf Apple-Hardware." -4. Kostenstruktur: "18 Mitarbeiter in 2030 bei 8.4 Mio EUR Umsatz." +4. Kostenstruktur: "12 Mitarbeiter in 2030 bei 7.8 Mio EUR Umsatz." 5. Marktchance: "12.4 Mrd EUR TAM, regulatorisch getrieben." +6. Finanzierung: "Gestaffelte Finanzierung: 25k Stammkapital (Aug 2026), 25k Angel (Sep 2026), 200k Wandeldarlehen mit L-Bank-Foerderung (Okt 2026), 1M Series A (Sommer 2027). Gesamt: 1,25 Mio EUR." +7. Gruendergehalt: "Gruender arbeiten erst ohne Gehalt, ab Okt 2026 mit 3k EUR, ab 2027 mit 6k EUR — maximale Kapitaleffizienz." ## Kommunikationsstil - Professionell, knapp und ueberzeugend @@ -34,7 +36,38 @@ Du hast Zugriff auf alle Unternehmensdaten und zitierst immer konkrete Zahlen. NIEMALS offenbaren: Exakte Modellnamen, Frameworks, Code-Architektur, Datenbankschema, Sicherheitsdetails, Cloud-Provider. Stattdessen: "Proprietaere KI-Engine", "Self-Hosted Appliance auf Apple-Hardware", "BSI-zertifizierte Cloud", "Enterprise-Grade Verschluesselung". -## Erlaubt: Geschaeftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (oeffentlich), LLM-Groessen (32b/40b/1000b).` +## Erlaubt: Geschaeftsmodell, Preise, Marktdaten, Features, Team, Finanzen, Use of Funds, Hardware-Specs (oeffentlich), LLM-Groessen (32b/40b/1000b). + +## Slide-Awareness (IMMER beachten) +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: +- Beantworte kurz, dann: "Details dazu finden Sie in Slide X: [Name]. Moechten Sie dorthin springen? [GOTO:X]" + +## FOLLOW-UP FRAGEN — KRITISCHE PFLICHT + +Du MUSST am Ende JEDER einzelnen Antwort exakt 3 Folgefragen anhaengen. +Die Fragen muessen durch "---" getrennt und mit "[Q]" markiert sein. +JEDE Antwort ohne Folgefragen ist UNVOLLSTAENDIG und FEHLERHAFT. + +EXAKTES FORMAT (keine Abweichung erlaubt): + +[Deine Antwort hier] + +--- +[Q] Erste Folgefrage passend zum Thema? +[Q] Zweite Folgefrage die tiefer geht? +[Q] Dritte Folgefrage zu einem verwandten Aspekt? + +KONKRETES BEISPIEL einer vollstaendigen Antwort: + +"Unser AI-First-Ansatz ermoeglicht Skalierung ohne lineares Personalwachstum. Der Umsatz steigt von 36k EUR (2026) auf 8.4 Mio EUR (2030), waehrend das Team nur von 2 auf 18 Personen waechst. + +--- +[Q] Wie sieht die Kostenstruktur im Detail aus? +[Q] Welche Unit Economics erreicht ihr in 2030? +[Q] Wie vergleicht sich die Personaleffizienz mit Wettbewerbern?" + +WICHTIG: Vergiss NIEMALS die Folgefragen! Sie sind PFLICHT.` async function loadPitchContext(): Promise { try { @@ -86,7 +119,7 @@ ${JSON.stringify(features.rows, null, 2)} export async function POST(request: NextRequest) { try { const body = await request.json() - const { message, history = [], lang = 'de' } = body + const { message, history = [], lang = 'de', slideContext } = body if (!message || typeof message !== 'string') { return NextResponse.json({ error: 'Message is required' }, { status: 400 }) @@ -98,6 +131,53 @@ export async function POST(request: NextRequest) { if (pitchContext) { systemContent += '\n' + pitchContext } + // Slide context for contextual awareness + if (slideContext) { + const SLIDE_NAMES: Record = { + '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 }, + 'technology': { de: 'Technologie', en: 'Technology', index: 10 }, + 'financials': { de: 'Finanzen', en: 'Financials', index: 11 }, + 'the-ask': { de: 'The Ask', en: 'The Ask', index: 12 }, + 'ai-qa': { de: 'KI Q&A', en: 'AI Q&A', index: 13 }, + 'appendix': { de: 'Appendix', en: 'Appendix', index: 14 }, + 'annex-infra': { de: 'Infrastruktur', en: 'Infrastructure', index: 15 }, + 'annex-ai-stack': { de: 'KI-Stack', en: 'AI Stack', index: 16 }, + 'annex-rag': { de: 'RAG Pipeline', en: 'RAG Pipeline', index: 17 }, + 'annex-security': { de: 'Sicherheit', en: 'Security', index: 18 }, + 'annex-devops': { de: 'DevOps & CI/CD', en: 'DevOps & CI/CD', index: 19 }, + 'annex-agent-arch': { de: 'Agent Architektur', en: 'Agent Architecture', index: 20 }, + 'annex-agent-rag': { de: 'Rechtsdokumente', en: 'Legal Documents', index: 21 }, + 'annex-agent-workflow': { de: 'Compliance Workflow', en: 'Compliance Workflow', index: 22 }, + 'annex-usp-overview': { de: '5 USPs', en: '5 USPs', index: 23 }, + 'annex-usp-comparison': { de: 'Wettbewerbsvergleich', en: 'Competitor Comparison', index: 24 }, + 'annex-usp-moat': { de: 'Marktposition', en: 'Market Position', index: 25 }, + 'annex-roadmap-2027': { de: 'Roadmap 2027', en: 'Roadmap 2027', index: 26 }, + 'annex-roadmap-2028': { de: 'Roadmap 2028', en: 'Roadmap 2028', index: 27 }, + } + const slideKeys = Object.keys(SLIDE_NAMES) + const visited: number[] = slideContext.visitedSlides || [] + const currentSlideName = SLIDE_NAMES[slideContext.currentSlide]?.[lang] || slideContext.currentSlide + const notYetSeen = Object.entries(SLIDE_NAMES) + .filter(([, v]) => !visited.includes(v.index)) + .map(([, v]) => `${v.index + 1}. ${v[lang]}`) + + systemContent += `\n\n## Slide-Kontext (WICHTIG fuer kontextuelle Antworten) +- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von 28) +- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_NAMES[slideKeys[i]]?.[lang]).filter(Boolean).join(', ')} +- Noch nicht gesehene Slides: ${notYetSeen.join(', ')} +- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geoeffnet' : 'Nein'} +` + } + systemContent += `\n\n## Aktuelle Sprache: ${lang === 'de' ? 'Deutsch' : 'English'}\nAntworte in ${lang === 'de' ? 'Deutsch' : 'English'}.` const messages = [ @@ -106,7 +186,7 @@ export async function POST(request: NextRequest) { role: h.role === 'user' ? 'user' : 'assistant', content: h.content, })), - { role: 'user', content: message }, + { 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`, { diff --git a/pitch-deck/app/api/financial-model/assumptions/route.ts b/pitch-deck/app/api/financial-model/assumptions/route.ts new file mode 100644 index 0000000..3ac8d35 --- /dev/null +++ b/pitch-deck/app/api/financial-model/assumptions/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +// PUT: Update a single assumption and trigger recompute +export async function PUT(request: NextRequest) { + try { + const body = await request.json() + const { scenarioId, key, value } = body + + if (!scenarioId || !key || value === undefined) { + return NextResponse.json({ error: 'scenarioId, key, and value are required' }, { status: 400 }) + } + + const client = await pool.connect() + try { + const jsonValue = JSON.stringify(value) + await client.query( + 'UPDATE pitch_fm_assumptions SET value = $1 WHERE scenario_id = $2 AND key = $3', + [jsonValue, scenarioId, key] + ) + + return NextResponse.json({ success: true }) + } finally { + client.release() + } + } catch (error) { + console.error('Update assumption error:', error) + return NextResponse.json({ error: 'Failed to update assumption' }, { status: 500 }) + } +} diff --git a/pitch-deck/app/api/financial-model/compute/route.ts b/pitch-deck/app/api/financial-model/compute/route.ts new file mode 100644 index 0000000..507ac1c --- /dev/null +++ b/pitch-deck/app/api/financial-model/compute/route.ts @@ -0,0 +1,388 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +interface FundingEvent { + month: number + amount: number + label: string +} + +interface SalaryStep { + from_month: number + salary: number +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { scenarioId } = body + + if (!scenarioId) { + return NextResponse.json({ error: 'scenarioId is required' }, { status: 400 }) + } + + const client = await pool.connect() + try { + // Load assumptions + const assumptionsRes = await client.query( + 'SELECT key, value, value_type FROM pitch_fm_assumptions WHERE scenario_id = $1', + [scenarioId] + ) + + const a: Record = {} + for (const row of assumptionsRes.rows) { + const val = typeof row.value === 'string' ? JSON.parse(row.value) : row.value + a[row.key] = val + } + + // Funding schedule (staged capital injections) + const fundingSchedule: FundingEvent[] = Array.isArray(a.funding_schedule) + ? (a.funding_schedule as FundingEvent[]) + : [ + { month: 8, amount: 25000, label: 'Stammkapital GmbH' }, + { month: 9, amount: 25000, label: 'Angel-Runde' }, + { month: 10, amount: 200000, label: 'Wandeldarlehen (Investor + L-Bank)' }, + { month: 19, amount: 1000000, label: 'Series A' }, + ] + + // Founder salary schedule (per founder) + const founderSalarySchedule: SalaryStep[] = Array.isArray(a.founder_salary_schedule) + ? (a.founder_salary_schedule as SalaryStep[]) + : [ + { from_month: 1, salary: 0 }, + { from_month: 10, salary: 3000 }, + { from_month: 13, salary: 6000 }, + { from_month: 25, salary: 10000 }, + ] + + const numFounders = Number(a.num_founders) || 2 + + // Extract scalar values + const growthRate = (Number(a.monthly_growth_rate) || 15) / 100 + const churnRate = (Number(a.churn_rate_monthly) || 3) / 100 + const arpuMini = Number(a.arpu_mini) || 299 + const arpuStudio = Number(a.arpu_studio) || 999 + const arpuCloud = Number(a.arpu_cloud) || 1499 + const mixMini = (Number(a.product_mix_mini) || 60) / 100 + const mixStudio = (Number(a.product_mix_studio) || 25) / 100 + const mixCloud = (Number(a.product_mix_cloud) || 15) / 100 + const initialCustomers = Number(a.initial_customers) || 2 + const cac = Number(a.cac) || 500 + const hwCostMini = Number(a.hw_cost_per_mini) || 3200 + const hwCostStudio = Number(a.hw_cost_per_studio) || 12000 + const cloudOpex = Number(a.cloud_opex_per_customer) || 150 + const salaryAvg = Number(a.salary_avg_monthly) || 6000 + // Hiring plan: employees EXCLUDING founders (hired staff only) + const hiringPlan: number[] = Array.isArray(a.hiring_plan) ? (a.hiring_plan as number[]) : [0, 1, 3, 6, 10] + const marketingMonthly = Number(a.marketing_monthly) || 2000 + const infraBase = Number(a.infra_monthly_base) || 500 + + // Detail cost assumptions + const ihkAnnual = Number(a.ihk_annual) || 180 + const phoneInternetMonthly = Number(a.phone_internet_monthly) || 100 + const taxAdvisorMonthly = Number(a.tax_advisor_monthly) || 300 + const notaryFounding = Number(a.notary_founding) || 2500 + const insuranceMonthly = Number(a.insurance_monthly) || 200 + const officeRentMonthly = Number(a.office_rent_monthly) || 0 + const softwareLicensesMonthly = Number(a.software_licenses_monthly) || 150 + const travelMonthly = Number(a.travel_monthly) || 200 + const legalMonthly = Number(a.legal_monthly) || 100 + const depreciationRatePct = Number(a.depreciation_rate_pct) || 33 + const taxRatePct = Number(a.tax_rate_pct) || 30 + const interestRatePct = Number(a.interest_rate_pct) || 5 + // Hardware financing: only this % paid upfront, rest via leasing/financing + const hwUpfrontPct = (Number(a.hw_upfront_pct) || 30) / 100 + + // GmbH founding month (month 8 = August 2026) + const gmbhFoundingMonth = 8 + + // Weighted ARPU + const weightedArpu = arpuMini * mixMini + arpuStudio * mixStudio + arpuCloud * mixCloud + + // Weighted hardware cost (only for Mini and Studio — Cloud is OpEx) + const hwCostWeighted = hwCostMini * mixMini + hwCostStudio * mixStudio + + // Helper: get founder salary for a given month + function getFounderSalary(month: number): number { + let salary = 0 + for (const step of founderSalarySchedule) { + if (month >= step.from_month) { + salary = step.salary + } + } + return salary + } + + // Helper: get funding for a given month + function getFundingForMonth(month: number): number { + let total = 0 + for (const event of fundingSchedule) { + if (event.month === month) { + total += event.amount + } + } + return total + } + + const results = [] + let totalCustomers = 0 + let cashBalance = 0 // Start at 0, funding comes via schedule + let cumulativeRevenue = 0 + let breakEvenMonth: number | null = null + let peakBurn = 0 + let cumulativeHwInvestment = 0 + + for (let m = 1; m <= 60; m++) { + const yearIndex = Math.floor((m - 1) / 12) // 0-4 + const year = 2026 + yearIndex + const monthInYear = ((m - 1) % 12) + 1 + + // === FUNDING: Add capital injection for this month === + const fundingThisMonth = getFundingForMonth(m) + cashBalance += fundingThisMonth + + // === PRE-GMBH PHASE (months 1-7): No costs, private development === + if (m < gmbhFoundingMonth) { + results.push({ + month: m, + year, + month_in_year: monthInYear, + new_customers: 0, + churned_customers: 0, + total_customers: 0, + mrr_eur: 0, + arr_eur: 0, + revenue_eur: 0, + cogs_eur: 0, + personnel_eur: 0, + infra_eur: 0, + marketing_eur: 0, + total_costs_eur: 0, + employees_count: 0, + gross_margin_pct: 0, + burn_rate_eur: 0, + runway_months: 999, + cac_eur: 0, + ltv_eur: 0, + ltv_cac_ratio: 0, + cash_balance_eur: Math.round(cashBalance * 100) / 100, + cumulative_revenue_eur: 0, + admin_costs_eur: 0, + office_costs_eur: 0, + founding_costs_eur: 0, + ihk_eur: 0, + depreciation_eur: 0, + interest_expense_eur: 0, + taxes_eur: 0, + net_income_eur: 0, + ebit_eur: 0, + software_licenses_eur: 0, + travel_costs_eur: 0, + funding_eur: fundingThisMonth, + }) + continue + } + + // === POST-GMBH PHASE (months 8+): Real business operations === + + // Hired employees: plan-based but capped by revenue (don't hire ahead of revenue) + const plannedHires = hiringPlan[Math.min(yearIndex, hiringPlan.length - 1)] || 0 + const revenueBasedMaxHires = Math.floor((totalCustomers * weightedArpu) / salaryAvg) + const hiredEmployees = Math.min(plannedHires, Math.max(0, revenueBasedMaxHires)) + + // Founder salary + const founderSalaryPerPerson = getFounderSalary(m) + const totalFounderSalary = founderSalaryPerPerson * numFounders + + // Total employees shown (founders + hired) + const totalEmployees = numFounders + hiredEmployees + + // Customer dynamics — start acquiring customers from GmbH founding + const monthsSinceGmbh = m - gmbhFoundingMonth + 1 + if (monthsSinceGmbh === 1) { + totalCustomers = initialCustomers + } + + let newCustomers = monthsSinceGmbh === 1 + ? initialCustomers + : Math.max(1, Math.round(totalCustomers * growthRate)) + + // Cash constraint: don't spend more than available + // Fixed OPEX this month (independent of new customer count) + const fixedOpex = (totalFounderSalary + hiredEmployees * salaryAvg) + + marketingMonthly + + infraBase + (totalCustomers * 5) + + (totalCustomers * mixCloud * cloudOpex) + + phoneInternetMonthly + taxAdvisorMonthly + insuranceMonthly + legalMonthly + + officeRentMonthly + (m === gmbhFoundingMonth ? notaryFounding : 0) + + ihkAnnual / 12 + softwareLicensesMonthly + travelMonthly + const estRevenue = totalCustomers * weightedArpu + // Available cash = current balance + this month's revenue - fixed costs + const availableCash = cashBalance + estRevenue - fixedOpex + // Variable cost per new customer: hardware CAPEX (upfront portion) + CAC + const varCostPerNew = hwCostWeighted * hwUpfrontPct + cac + // Max affordable new customers (keep cash >= 0) + if (varCostPerNew > 0 && monthsSinceGmbh > 1) { + const maxAffordable = Math.floor(availableCash / varCostPerNew) + newCustomers = Math.min(newCustomers, Math.max(1, maxAffordable)) + } + + const churned = Math.round(totalCustomers * churnRate) + if (monthsSinceGmbh > 1) { + totalCustomers = totalCustomers + newCustomers - churned + } + totalCustomers = Math.max(0, totalCustomers) + + // Revenue + const mrr = totalCustomers * weightedArpu + const arr = mrr * 12 + const revenue = mrr + + // Costs + const hiredPersonnelCost = hiredEmployees * salaryAvg + const personnelCost = totalFounderSalary + hiredPersonnelCost + + // Hardware = CAPEX (only upfront portion paid from cash, rest financed) + const capexHardware = newCustomers * hwCostWeighted * hwUpfrontPct + // Cloud OPEX + hardware leasing cost (financed portion amortized over 36 months) + const cogsCloud = totalCustomers * mixCloud * cloudOpex + const hwLeasingMonthly = (cumulativeHwInvestment * (1 - hwUpfrontPct)) / 36 + const cogs = cogsCloud + hwLeasingMonthly + const marketingCost = marketingMonthly + (newCustomers * cac) + const infraCost = infraBase + (totalCustomers * 5) + + // Detail costs + const adminCosts = phoneInternetMonthly + taxAdvisorMonthly + insuranceMonthly + legalMonthly + const officeCosts = officeRentMonthly + // Founding costs: notary in month 8 (GmbH founding) + const foundingCosts = m === gmbhFoundingMonth ? notaryFounding : 0 + const ihkMonthly = ihkAnnual / 12 + const softwareLicenses = softwareLicensesMonthly + const travelCosts = travelMonthly + + // Depreciation: cumulative HW investment * rate / 12 (P&L expense for CAPEX) + cumulativeHwInvestment += capexHardware + const depreciationMonthly = (cumulativeHwInvestment * depreciationRatePct / 100) / 12 + + // Total OPEX (P&L) — hardware enters only via depreciation + const totalCosts = personnelCost + cogs + marketingCost + infraCost + + adminCosts + officeCosts + foundingCosts + ihkMonthly + + softwareLicenses + travelCosts + depreciationMonthly + + // EBIT + const ebit = revenue - totalCosts + + // Interest expense (only if cash balance is negative) + const interestExpense = cashBalance < 0 ? Math.abs(cashBalance) * interestRatePct / 100 / 12 : 0 + + // Taxes (only if profit positive) + const ebt = ebit - interestExpense + const taxes = ebt > 0 ? ebt * taxRatePct / 100 : 0 + + // Net income + const netIncome = ebt - taxes + + // Cash: net income MINUS hardware CAPEX (funding already added at top) + cashBalance += netIncome - capexHardware + cumulativeRevenue += revenue + + // KPIs — gross margin uses COGS + depreciation for true margin + const grossMargin = revenue > 0 ? ((revenue - cogs - depreciationMonthly) / revenue) * 100 : 0 + const burnRate = (netIncome - capexHardware) < 0 ? Math.abs(netIncome - capexHardware) : 0 + const runway = burnRate > 0 ? cashBalance / burnRate : 999 + const avgLifetimeMonths = churnRate > 0 ? 1 / churnRate : 60 + const ltv = weightedArpu * avgLifetimeMonths + const ltvCacRatio = cac > 0 ? ltv / cac : 0 + + if (peakBurn < burnRate) peakBurn = burnRate + + // Break-even detection + if (breakEvenMonth === null && netIncome >= 0 && m > gmbhFoundingMonth) { + breakEvenMonth = m + } + + results.push({ + month: m, + year, + month_in_year: monthInYear, + new_customers: newCustomers, + churned_customers: churned, + total_customers: totalCustomers, + mrr_eur: Math.round(mrr * 100) / 100, + arr_eur: Math.round(arr * 100) / 100, + revenue_eur: Math.round(revenue * 100) / 100, + cogs_eur: Math.round(cogs * 100) / 100, + personnel_eur: Math.round(personnelCost * 100) / 100, + infra_eur: Math.round(infraCost * 100) / 100, + marketing_eur: Math.round(marketingCost * 100) / 100, + total_costs_eur: Math.round(totalCosts * 100) / 100, + employees_count: totalEmployees, + gross_margin_pct: Math.round(grossMargin * 100) / 100, + burn_rate_eur: Math.round(burnRate * 100) / 100, + runway_months: Math.round(Math.min(runway, 999) * 10) / 10, + cac_eur: cac, + ltv_eur: Math.round(ltv * 100) / 100, + ltv_cac_ratio: Math.round(ltvCacRatio * 100) / 100, + cash_balance_eur: Math.round(cashBalance * 100) / 100, + cumulative_revenue_eur: Math.round(cumulativeRevenue * 100) / 100, + // Detail costs + admin_costs_eur: Math.round(adminCosts * 100) / 100, + office_costs_eur: Math.round(officeCosts * 100) / 100, + founding_costs_eur: Math.round(foundingCosts * 100) / 100, + ihk_eur: Math.round(ihkMonthly * 100) / 100, + depreciation_eur: Math.round(depreciationMonthly * 100) / 100, + interest_expense_eur: Math.round(interestExpense * 100) / 100, + taxes_eur: Math.round(taxes * 100) / 100, + net_income_eur: Math.round(netIncome * 100) / 100, + ebit_eur: Math.round(ebit * 100) / 100, + software_licenses_eur: Math.round(softwareLicenses * 100) / 100, + travel_costs_eur: Math.round(travelCosts * 100) / 100, + funding_eur: fundingThisMonth, + }) + } + + // Save to DB (upsert) — only columns that exist in the table + await client.query('DELETE FROM pitch_fm_results WHERE scenario_id = $1', [scenarioId]) + for (const r of results) { + await client.query(` + INSERT INTO pitch_fm_results (scenario_id, month, year, month_in_year, + new_customers, churned_customers, total_customers, + mrr_eur, arr_eur, revenue_eur, + cogs_eur, personnel_eur, infra_eur, marketing_eur, total_costs_eur, + employees_count, gross_margin_pct, burn_rate_eur, runway_months, + cac_eur, ltv_eur, ltv_cac_ratio, + cash_balance_eur, cumulative_revenue_eur) + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24) + `, [ + scenarioId, r.month, r.year, r.month_in_year, + r.new_customers, r.churned_customers, r.total_customers, + r.mrr_eur, r.arr_eur, r.revenue_eur, + r.cogs_eur, r.personnel_eur, r.infra_eur, r.marketing_eur, r.total_costs_eur, + r.employees_count, r.gross_margin_pct, r.burn_rate_eur, r.runway_months, + r.cac_eur, r.ltv_eur, r.ltv_cac_ratio, + r.cash_balance_eur, r.cumulative_revenue_eur, + ]) + } + + const lastResult = results[results.length - 1] + return NextResponse.json({ + scenario_id: scenarioId, + results, + summary: { + final_arr: lastResult.arr_eur, + final_customers: lastResult.total_customers, + break_even_month: breakEvenMonth, + final_runway: lastResult.runway_months, + final_ltv_cac: lastResult.ltv_cac_ratio, + peak_burn: Math.round(peakBurn * 100) / 100, + total_funding_needed: Math.round(Math.abs(Math.min(...results.map(r => r.cash_balance_eur), 0)) * 100) / 100, + }, + }) + } finally { + client.release() + } + } catch (error) { + console.error('Compute error:', error) + return NextResponse.json({ error: 'Computation failed' }, { status: 500 }) + } +} diff --git a/pitch-deck/app/api/financial-model/results/[scenarioId]/route.ts b/pitch-deck/app/api/financial-model/results/[scenarioId]/route.ts new file mode 100644 index 0000000..c8f2dd7 --- /dev/null +++ b/pitch-deck/app/api/financial-model/results/[scenarioId]/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +export const dynamic = 'force-dynamic' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ scenarioId: string }> } +) { + try { + const { scenarioId } = await params + + const client = await pool.connect() + try { + const results = await client.query( + 'SELECT * FROM pitch_fm_results WHERE scenario_id = $1 ORDER BY month', + [scenarioId] + ) + + return NextResponse.json(results.rows) + } finally { + client.release() + } + } catch (error) { + console.error('Load results error:', error) + return NextResponse.json({ error: 'Failed to load results' }, { status: 500 }) + } +} diff --git a/pitch-deck/app/api/financial-model/route.ts b/pitch-deck/app/api/financial-model/route.ts new file mode 100644 index 0000000..a74caaf --- /dev/null +++ b/pitch-deck/app/api/financial-model/route.ts @@ -0,0 +1,73 @@ +import { NextRequest, NextResponse } from 'next/server' +import pool from '@/lib/db' + +export const dynamic = 'force-dynamic' + +// GET: Load all scenarios with their assumptions +export async function GET() { + try { + const client = await pool.connect() + try { + const scenarios = await client.query( + 'SELECT * FROM pitch_fm_scenarios ORDER BY is_default DESC, name' + ) + + const assumptions = await client.query( + 'SELECT * FROM pitch_fm_assumptions ORDER BY sort_order' + ) + + const result = scenarios.rows.map(s => ({ + ...s, + assumptions: assumptions.rows + .filter(a => a.scenario_id === s.id) + .map(a => ({ + ...a, + value: typeof a.value === 'string' ? JSON.parse(a.value) : a.value, + })), + })) + + return NextResponse.json(result) + } finally { + client.release() + } + } catch (error) { + console.error('Financial model load error:', error) + return NextResponse.json({ error: 'Failed to load scenarios' }, { status: 500 }) + } +} + +// POST: Create a new scenario +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, description, color, copyFrom } = body + + if (!name) { + return NextResponse.json({ error: 'Name is required' }, { status: 400 }) + } + + const client = await pool.connect() + try { + const scenario = await client.query( + 'INSERT INTO pitch_fm_scenarios (name, description, color) VALUES ($1, $2, $3) RETURNING *', + [name, description || '', color || '#6366f1'] + ) + + // If copyFrom is set, copy assumptions from another scenario + if (copyFrom) { + await client.query(` + INSERT INTO pitch_fm_assumptions (scenario_id, key, label_de, label_en, value, value_type, unit, min_value, max_value, step_size, category, sort_order) + SELECT $1, key, label_de, label_en, value, value_type, unit, min_value, max_value, step_size, category, sort_order + FROM pitch_fm_assumptions WHERE scenario_id = $2 + `, [scenario.rows[0].id, copyFrom]) + } + + return NextResponse.json(scenario.rows[0]) + } finally { + client.release() + } + } catch (error) { + console.error('Create scenario error:', error) + return NextResponse.json({ error: 'Failed to create scenario' }, { status: 500 }) + } +} diff --git a/pitch-deck/components/ChatFAB.tsx b/pitch-deck/components/ChatFAB.tsx new file mode 100644 index 0000000..fe65677 --- /dev/null +++ b/pitch-deck/components/ChatFAB.tsx @@ -0,0 +1,420 @@ +'use client' + +import { useState, useRef, useEffect, useMemo } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { X, Send, Bot, User, Sparkles, Maximize2, Minimize2, ArrowRight } from 'lucide-react' +import { ChatMessage, Language, SlideId } from '@/lib/types' +import { t } from '@/lib/i18n' + +interface ChatFABProps { + lang: Language + currentSlide: SlideId + currentIndex: number + visitedSlides: Set + onGoToSlide: (index: number) => void +} + +interface ParsedMessage { + text: string + followUps: string[] + gotos: { index: number; label: string }[] +} + +function parseAgentResponse(content: string, lang: Language): ParsedMessage { + const followUps: string[] = [] + const gotos: { index: number; label: string }[] = [] + + // Split on the follow-up separator — flexible: "---", "- - -", "___", or multiple dashes + const parts = content.split(/\n\s*[-_]{3,}\s*\n/) + let text = parts[0] + + // Parse follow-up questions from second part + if (parts.length > 1) { + const qSection = parts.slice(1).join('\n') + // Match [Q], **[Q]**, or numbered/bulleted question patterns + const qMatches = qSection.matchAll(/(?:\[Q\]|\*\*\[Q\]\*\*)\s*(.+?)(?:\n|$)/g) + for (const m of qMatches) { + const q = m[1].trim().replace(/^\*\*|\*\*$/g, '') + if (q.length > 5) followUps.push(q) + } + + // Fallback: if no [Q] markers found, look for numbered or bulleted questions in the section + if (followUps.length === 0) { + const lineMatches = qSection.matchAll(/(?:^|\n)\s*(?:\d+[\.\)]\s*|[-•]\s*)(.+?\?)\s*$/gm) + for (const m of lineMatches) { + const q = m[1].trim() + if (q.length > 5 && followUps.length < 3) followUps.push(q) + } + } + } + + // Also look for [Q] questions anywhere in the text (sometimes model puts them without ---) + if (followUps.length === 0) { + const inlineMatches = content.matchAll(/\[Q\]\s*(.+?)(?:\n|$)/g) + const inlineQs: string[] = [] + for (const m of inlineMatches) { + inlineQs.push(m[1].trim()) + } + if (inlineQs.length >= 2) { + followUps.push(...inlineQs) + // Remove [Q] lines from main text + text = text.replace(/\n?\s*\[Q\]\s*.+?(?:\n|$)/g, '\n').trim() + } + } + + // Parse GOTO markers from the text + const gotoRegex = /\[GOTO:(\d+)\]/g + let gotoMatch + while ((gotoMatch = gotoRegex.exec(text)) !== null) { + const slideIndex = parseInt(gotoMatch[1]) + gotos.push({ + index: slideIndex, + label: lang === 'de' ? `Zu Slide ${slideIndex + 1} springen` : `Jump to slide ${slideIndex + 1}`, + }) + } + // Remove GOTO markers from visible text + text = text.replace(/\s*\[GOTO:\d+\]/g, '') + + // Clean up trailing reminder instruction that might leak through + text = text.replace(/\n*\(Erinnerung:.*?\)\s*$/s, '').trim() + + return { text: text.trim(), followUps, gotos } +} + +export default function ChatFAB({ lang, currentSlide, currentIndex, visitedSlides, onGoToSlide }: ChatFABProps) { + const i = t(lang) + const [isOpen, setIsOpen] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const [messages, setMessages] = useState([]) + const [input, setInput] = useState('') + const [isStreaming, setIsStreaming] = useState(false) + const [parsedResponses, setParsedResponses] = useState>(new Map()) + const messagesEndRef = useRef(null) + const inputRef = useRef(null) + const abortRef = useRef(null) + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages, parsedResponses]) + + useEffect(() => { + if (isOpen && inputRef.current) { + setTimeout(() => inputRef.current?.focus(), 200) + } + }, [isOpen]) + + // Parse the latest assistant message when streaming ends + const lastAssistantIndex = useMemo(() => { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'assistant') return i + } + return -1 + }, [messages]) + + useEffect(() => { + if (!isStreaming && lastAssistantIndex >= 0 && !parsedResponses.has(lastAssistantIndex)) { + const msg = messages[lastAssistantIndex] + const parsed = parseAgentResponse(msg.content, lang) + setParsedResponses(prev => new Map(prev).set(lastAssistantIndex, parsed)) + } + }, [isStreaming, lastAssistantIndex, messages, parsedResponses, lang]) + + async function sendMessage(text?: string) { + const message = text || input.trim() + if (!message || isStreaming) return + + setInput('') + setMessages(prev => [...prev, { role: 'user', content: message }]) + setIsStreaming(true) + + abortRef.current = new AbortController() + + try { + const res = await fetch('/api/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message, + history: messages.slice(-10), + lang, + slideContext: { + currentSlide, + currentIndex, + visitedSlides: Array.from(visitedSlides), + totalSlides: 13, + }, + }), + signal: abortRef.current.signal, + }) + + if (!res.ok) throw new Error(`HTTP ${res.status}`) + + const reader = res.body!.getReader() + const decoder = new TextDecoder() + let content = '' + + setMessages(prev => [...prev, { role: 'assistant', content: '' }]) + + while (true) { + const { done, value } = await reader.read() + if (done) break + + content += decoder.decode(value, { stream: true }) + const currentText = content + setMessages(prev => { + const updated = [...prev] + updated[updated.length - 1] = { role: 'assistant', content: currentText } + return updated + }) + } + } catch (err: unknown) { + if (err instanceof Error && err.name === 'AbortError') return + console.error('Chat error:', err) + setMessages(prev => [ + ...prev, + { role: 'assistant', content: lang === 'de' + ? 'Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.' + : 'Connection failed. Please try again.' + }, + ]) + } finally { + setIsStreaming(false) + abortRef.current = null + } + } + + function stopGeneration() { + if (abortRef.current) { + abortRef.current.abort() + setIsStreaming(false) + } + } + + const suggestions = i.aiqa.suggestions.slice(0, 3) + + function renderMessageContent(msg: ChatMessage, idx: number) { + const parsed = parsedResponses.get(idx) + const displayText = parsed ? parsed.text : msg.content + + return ( + <> +
{displayText}
+ {isStreaming && idx === messages.length - 1 && msg.role === 'assistant' && ( + + )} + + {/* GOTO Buttons */} + {parsed && parsed.gotos.length > 0 && ( +
+ {parsed.gotos.map((g, gi) => ( + + ))} +
+ )} + + {/* Follow-Up Suggestions */} + {parsed && parsed.followUps.length > 0 && !isStreaming && ( +
+ {parsed.followUps.map((q, qi) => ( + + ))} +
+ )} + + ) + } + + return ( + <> + {/* FAB Button — sits to the left of NavigationFAB */} + + {!isOpen && ( + setIsOpen(true)} + className="fixed bottom-6 right-[5.5rem] z-50 w-14 h-14 rounded-full + bg-indigo-600 hover:bg-indigo-500 text-white + flex items-center justify-center shadow-lg shadow-indigo-600/30 + transition-colors" + aria-label={lang === 'de' ? 'Investor Agent oeffnen' : 'Open Investor Agent'} + > + + + + + + + + )} + + + {/* Chat Panel */} + + {isOpen && ( + + {/* Header */} +
+
+
+ +
+
+ Investor Agent + + {isStreaming + ? (lang === 'de' ? 'antwortet...' : 'responding...') + : (lang === 'de' ? 'online' : 'online') + } + +
+
+
+ + +
+
+ + {/* Messages */} +
+ {messages.length === 0 && ( +
+
+ + {lang === 'de' ? 'Fragen Sie den Investor Agent:' : 'Ask the Investor Agent:'} +
+ {suggestions.map((q, idx) => ( + sendMessage(q)} + className="block w-full text-left px-3 py-2.5 rounded-xl + bg-white/[0.05] border border-white/10 + hover:bg-white/[0.1] transition-colors + text-xs text-white/70 hover:text-white" + > + {q} + + ))} +
+ )} + + {messages.map((msg, idx) => ( +
+ {msg.role === 'assistant' && ( +
+ +
+ )} +
+ {msg.role === 'assistant' ? renderMessageContent(msg, idx) : ( +
{msg.content}
+ )} +
+ {msg.role === 'user' && ( +
+ +
+ )} +
+ ))} +
+
+ + {/* Input */} +
+ {isStreaming && ( + + )} +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && sendMessage()} + placeholder={lang === 'de' ? 'Frage stellen...' : 'Ask a question...'} + disabled={isStreaming} + className="flex-1 bg-white/[0.06] border border-white/10 rounded-xl px-3.5 py-2.5 + text-xs text-white placeholder-white/30 outline-none + focus:border-indigo-500/50 focus:ring-1 focus:ring-indigo-500/20 + disabled:opacity-50 transition-all" + /> + +
+
+ + )} + + + ) +} diff --git a/pitch-deck/components/NavigationFAB.tsx b/pitch-deck/components/NavigationFAB.tsx index 268bdb4..f600d80 100644 --- a/pitch-deck/components/NavigationFAB.tsx +++ b/pitch-deck/components/NavigationFAB.tsx @@ -83,38 +83,44 @@ export default function NavigationFAB({ {i.slideNames.map((name, idx) => { const isActive = idx === currentIndex const isVisited = visitedSlides.has(idx) - const isAI = idx === totalSlides - 1 + const isAI = idx === 13 return ( - + +
) })} diff --git a/pitch-deck/components/PitchDeck.tsx b/pitch-deck/components/PitchDeck.tsx index 78f831c..b888c2c 100644 --- a/pitch-deck/components/PitchDeck.tsx +++ b/pitch-deck/components/PitchDeck.tsx @@ -11,6 +11,7 @@ import ParticleBackground from './ParticleBackground' import ProgressBar from './ProgressBar' import NavigationControls from './NavigationControls' import NavigationFAB from './NavigationFAB' +import ChatFAB from './ChatFAB' import SlideOverview from './SlideOverview' import SlideContainer from './SlideContainer' @@ -24,9 +25,24 @@ import BusinessModelSlide from './slides/BusinessModelSlide' import TractionSlide from './slides/TractionSlide' import CompetitionSlide from './slides/CompetitionSlide' import TeamSlide from './slides/TeamSlide' +import TechnologySlide from './slides/TechnologySlide' import FinancialsSlide from './slides/FinancialsSlide' import TheAskSlide from './slides/TheAskSlide' import AIQASlide from './slides/AIQASlide' +import AppendixSlide from './slides/AppendixSlide' +import AnnexInfraSlide from './slides/AnnexInfraSlide' +import AnnexAIStackSlide from './slides/AnnexAIStackSlide' +import AnnexRAGSlide from './slides/AnnexRAGSlide' +import AnnexSecuritySlide from './slides/AnnexSecuritySlide' +import AnnexDevOpsSlide from './slides/AnnexDevOpsSlide' +import AnnexAgentArchSlide from './slides/AnnexAgentArchSlide' +import AnnexAgentRAGSlide from './slides/AnnexAgentRAGSlide' +import AnnexAgentWorkflowSlide from './slides/AnnexAgentWorkflowSlide' +import AnnexUSPOverviewSlide from './slides/AnnexUSPOverviewSlide' +import AnnexUSPComparisonSlide from './slides/AnnexUSPComparisonSlide' +import AnnexUSPMoatSlide from './slides/AnnexUSPMoatSlide' +import AnnexRoadmap2027Slide from './slides/AnnexRoadmap2027Slide' +import AnnexRoadmap2028Slide from './slides/AnnexRoadmap2028Slide' interface PitchDeckProps { lang: Language @@ -109,12 +125,42 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) { return case 'team': return + case 'technology': + return case 'financials': - return + return case 'the-ask': return case 'ai-qa': return + case 'appendix': + return + case 'annex-infra': + return + case 'annex-ai-stack': + return + case 'annex-rag': + return + case 'annex-security': + return + case 'annex-devops': + return + case 'annex-agent-arch': + return + case 'annex-agent-rag': + return + case 'annex-agent-workflow': + return + case 'annex-usp-overview': + return + case 'annex-usp-comparison': + return + case 'annex-usp-moat': + return + case 'annex-roadmap-2027': + return + case 'annex-roadmap-2028': + return default: return null } @@ -138,6 +184,14 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) { total={nav.totalSlides} /> + + e.stopPropagation()} > {i.slideNames.map((name, idx) => ( - onGoToSlide(idx)} - className={` - aspect-video rounded-xl p-4 text-left - border transition-all - ${idx === currentIndex - ? 'bg-indigo-500/20 border-indigo-500 shadow-lg shadow-indigo-500/20' - : 'bg-white/[0.05] border-white/10 hover:bg-white/[0.1] hover:border-white/20' - } - `} - > - {idx + 1} - - {name} - - + + {idx === 14 && ( +
+ Appendix +
+
+ )} + onGoToSlide(idx)} + className={` + aspect-video rounded-xl p-4 text-left + border transition-all + ${idx === currentIndex + ? 'bg-indigo-500/20 border-indigo-500 shadow-lg shadow-indigo-500/20' + : 'bg-white/[0.05] border-white/10 hover:bg-white/[0.1] hover:border-white/20' + } + `} + > + {idx + 1} + + {name} + + + ))} diff --git a/pitch-deck/components/slides/AnnexAIStackSlide.tsx b/pitch-deck/components/slides/AnnexAIStackSlide.tsx new file mode 100644 index 0000000..535d16d --- /dev/null +++ b/pitch-deck/components/slides/AnnexAIStackSlide.tsx @@ -0,0 +1,200 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Brain, Cpu, Zap, ArrowRight } from 'lucide-react' + +interface AnnexAIStackSlideProps { + lang: Language +} + +export default function AnnexAIStackSlide({ lang }: AnnexAIStackSlideProps) { + const roadmapSteps = [ + { + year: '2026', + model: 'Qwen 32B', + label: lang === 'de' ? 'Foundation' : 'Foundation', + }, + { + year: '2027', + model: 'Multi-Model 70B', + label: lang === 'de' ? 'Skalierung' : 'Scaling', + }, + { + year: '2028', + model: 'Fine-Tuned 100B+', + label: 'Enterprise', + }, + { + year: '2030', + model: '1000B Agent Network', + label: lang === 'de' ? 'Volle Autonomie' : 'Full Autonomy', + }, + ] + + return ( +
+
+ +
+

+ + {lang === 'de' ? 'KI-Stack & LLM-Architektur' : 'AI Stack & LLM Architecture'} + +

+

+ {lang === 'de' ? 'Von 32B zu 1000B Parametern' : 'From 32B to 1000B parameters'} +

+
+
+ + {/* LLM Evolution Roadmap */} + +
+ {roadmapSteps.map((step, index) => ( +
+ +
{step.year}
+
{step.model}
+
{step.label}
+
+ + {index < roadmapSteps.length - 1 && ( + + + + )} +
+ ))} +
+
+ + {/* Bottom Section: 2-column grid */} +
+ {/* Left Card - Multi-Model Router */} + +
+
+ +
+

+ {lang === 'de' ? 'Multi-Model Router' : 'Multi-Model Router'} +

+
+
    +
  • + + + {lang === 'de' + ? 'Automatische Modellauswahl basierend auf Aufgabe' + : 'Automatic model selection based on task'} + +
  • +
  • + + + {lang === 'de' + ? 'Einfache Anfragen → kleineres Modell (schnell, günstig)' + : 'Simple queries → smaller model (fast, cheap)'} + +
  • +
  • + + + {lang === 'de' + ? 'Komplexe Analysen → größeres Modell (präzise)' + : 'Complex analysis → larger model (accurate)'} + +
  • +
  • + + + {lang === 'de' + ? 'Kostenoptimierung: 60% günstiger als immer großes Modell' + : 'Cost optimization: 60% cheaper than always using large model'} + +
  • +
+
+ + {/* Right Card - Inference Optimization */} + +
+
+ +
+

+ {lang === 'de' ? 'Inference-Optimierung' : 'Inference Optimization'} +

+
+
    +
  • + + + {lang === 'de' + ? 'Apple Neural Engine Beschleunigung' + : 'Apple Neural Engine acceleration'} + +
  • +
  • + + + {lang === 'de' + ? 'Quantisierung (INT4/INT8) für schnellere Inferenz' + : 'Quantization (INT4/INT8) for faster inference'} + +
  • +
  • + + Context window: 32k tokens +
  • +
  • + + + {lang === 'de' + ? 'Durchschn. Antwortzeit: <2s für einfache Anfragen' + : 'Avg response time: <2s for simple queries'} + +
  • +
  • + + + {lang === 'de' + ? 'GPU vs Apple Silicon: Apple 3x besseres Preis-/Leistungsverhältnis für Inferenz' + : 'GPU vs Apple Silicon: Apple 3x better price/performance for inference'} + +
  • +
+
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexAgentArchSlide.tsx b/pitch-deck/components/slides/AnnexAgentArchSlide.tsx new file mode 100644 index 0000000..d0ad073 --- /dev/null +++ b/pitch-deck/components/slides/AnnexAgentArchSlide.tsx @@ -0,0 +1,147 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Brain, GraduationCap, ClipboardCheck, Scale, Bell, Database, MessageSquare, Archive } from 'lucide-react' + +interface AnnexAgentArchSlideProps { + lang: Language +} + +export default function AnnexAgentArchSlide({ lang }: AnnexAgentArchSlideProps) { + const agents = [ + { + name: lang === 'de' ? 'Tutor Agent' : 'Tutor Agent', + icon: GraduationCap, + color: 'text-blue-400', + position: 'col-start-1 row-start-1' + }, + { + name: lang === 'de' ? 'Grader Agent' : 'Grader Agent', + icon: ClipboardCheck, + color: 'text-green-400', + position: 'col-start-3 row-start-1' + }, + { + name: lang === 'de' ? 'Quality Judge' : 'Quality Judge', + icon: Scale, + color: 'text-purple-400', + position: 'col-start-1 row-start-3' + }, + { + name: lang === 'de' ? 'Alert Agent' : 'Alert Agent', + icon: Bell, + color: 'text-amber-400', + position: 'col-start-3 row-start-3' + } + ] + + const features = [ + { + icon: Archive, + title: lang === 'de' ? 'Session Management' : 'Session Management', + desc: lang === 'de' ? 'Persistent state, auto-recovery, checkpoints' : 'Persistent state, auto-recovery, checkpoints' + }, + { + icon: Database, + title: lang === 'de' ? 'Shared Brain' : 'Shared Brain', + desc: lang === 'de' ? 'Long-term memory, knowledge graph, context sharing' : 'Long-term memory, knowledge graph, context sharing' + }, + { + icon: MessageSquare, + title: lang === 'de' ? 'Message Bus' : 'Message Bus', + desc: lang === 'de' ? 'Real-time inter-agent communication, priority routing' : 'Real-time inter-agent communication, priority routing' + } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Compliance Agent Architektur' : 'Compliance Agent Architecture'} + +

+

+ {lang === 'de' ? 'Multi-Agent System für autonome Compliance' : 'Multi-agent system for autonomous compliance'} +

+
+
+ + {/* Agent Architecture Diagram */} + +
+ {/* Top agents */} + {agents.slice(0, 2).map((agent, idx) => ( + + +

{agent.name}

+
+ ))} + + {/* Center Orchestrator */} + + +

Orchestrator

+

+ {lang === 'de' ? 'Koordiniert alle Agents' : 'Coordinates all agents'} +

+ + {/* Connection lines */} +
+
+
+
+ + + {/* Bottom agents */} + {agents.slice(2, 4).map((agent, idx) => ( + + +

{agent.name}

+
+ ))} +
+ + + {/* Features */} + +
+ {features.map((feature, idx) => ( + + +

{feature.title}

+

{feature.desc}

+
+ ))} +
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexAgentRAGSlide.tsx b/pitch-deck/components/slides/AnnexAgentRAGSlide.tsx new file mode 100644 index 0000000..d68c9fd --- /dev/null +++ b/pitch-deck/components/slides/AnnexAgentRAGSlide.tsx @@ -0,0 +1,145 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { FileText, Layers, Tag, RefreshCw } from 'lucide-react' + +interface AnnexAgentRAGSlideProps { + lang: Language +} + +export default function AnnexAgentRAGSlide({ lang }: AnnexAgentRAGSlideProps) { + const regulations = [ + { + name: 'DSGVO', + articles: lang === 'de' ? '99 Artikel' : '99 Articles', + recitals: lang === 'de' ? '173 Erwägungsgründe' : '173 Recitals', + color: 'from-blue-500/20 to-blue-600/20', + border: 'border-blue-400/30' + }, + { + name: 'AI Act', + articles: lang === 'de' ? '113 Artikel' : '113 Articles', + recitals: lang === 'de' ? '180 Erwägungsgründe' : '180 Recitals', + color: 'from-purple-500/20 to-purple-600/20', + border: 'border-purple-400/30' + }, + { + name: 'NIS2', + articles: lang === 'de' ? '46 Artikel' : '46 Articles', + recitals: lang === 'de' ? '144 Erwägungsgründe' : '144 Recitals', + color: 'from-green-500/20 to-green-600/20', + border: 'border-green-400/30' + } + ] + + const chunkingDetails = [ + { + icon: Layers, + label: lang === 'de' ? 'Chunk-Größe' : 'Chunk size', + value: lang === 'de' ? '512 Tokens mit 64 Token Überlappung' : '512 tokens with 64 token overlap' + }, + { + icon: Tag, + label: lang === 'de' ? 'Metadaten-Tagging' : 'Metadata tagging', + value: lang === 'de' ? 'Artikelnummer, Abschnitt, Verordnung, Sprache' : 'Article number, section, regulation, language' + }, + { + icon: FileText, + label: lang === 'de' ? 'Hierarchisches Chunking' : 'Hierarchical chunking', + value: lang === 'de' ? 'Artikel → Absatz → Satz' : 'Article → Paragraph → Sentence' + }, + { + icon: RefreshCw, + label: lang === 'de' ? 'Update-Workflow' : 'Update workflow', + value: lang === 'de' ? 'Automatische Re-Indizierung bei Gesetzesänderungen' : 'Automatic re-indexing on law changes' + } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Rechtsdokumente im RAG' : 'Legal Documents in RAG'} + +

+

+ {lang === 'de' ? 'Wie Gesetze in die KI gelangen' : 'How laws enter the AI'} +

+
+
+ + {/* Regulations */} + +
+ {regulations.map((reg, idx) => ( + +

{reg.name}

+
+
+

{reg.articles}

+
+
+

{reg.recitals}

+
+
+
+ ))} +
+
+ + {/* Chunking Strategy */} + + +

+ {lang === 'de' ? 'Chunking-Strategie' : 'Chunking Strategy'} +

+ +
+ {chunkingDetails.map((detail, idx) => ( + + +
+

{detail.label}

+

{detail.value}

+
+
+ ))} +
+ + +

+ {lang === 'de' ? 'Aktuell: 15.000+ Chunks indiziert' : 'Currently: 15,000+ chunks indexed'} +

+
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx b/pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx new file mode 100644 index 0000000..10353b1 --- /dev/null +++ b/pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx @@ -0,0 +1,162 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Upload, ScanLine, Search, AlertTriangle, FileText, UserCheck, ShieldCheck } from 'lucide-react' + +interface AnnexAgentWorkflowSlideProps { + lang: Language +} + +export default function AnnexAgentWorkflowSlide({ lang }: AnnexAgentWorkflowSlideProps) { + const workflowSteps = [ + { + icon: Upload, + title: 'Upload', + desc: lang === 'de' ? 'Dokument-Upload (Verträge, Richtlinien, Aufzeichnungen)' : 'Document upload (contracts, policies, records)', + color: 'text-blue-400' + }, + { + icon: ScanLine, + title: 'OCR', + desc: lang === 'de' ? 'Automatische Textextraktion (PaddleOCR)' : 'Automatic text extraction (PaddleOCR)', + color: 'text-purple-400' + }, + { + icon: Search, + title: lang === 'de' ? 'Analyse' : 'Analysis', + desc: lang === 'de' ? 'KI analysiert gegen DSGVO/AI Act/NIS2' : 'AI analyzes against DSGVO/AI Act/NIS2', + color: 'text-indigo-400' + }, + { + icon: AlertTriangle, + title: lang === 'de' ? 'Risikobewertung' : 'Risk Assessment', + desc: lang === 'de' ? 'Automatische Risikoklassifizierung (niedrig/mittel/hoch/kritisch)' : 'Automatic risk classification (low/medium/high/critical)', + color: 'text-amber-400' + }, + { + icon: FileText, + title: lang === 'de' ? 'Report-Generierung' : 'Report Generation', + desc: lang === 'de' ? 'DSFA, VVT, TOM Dokumente automatisch erstellt' : 'DSFA, VVT, TOM documents auto-generated', + color: 'text-green-400' + }, + { + icon: UserCheck, + title: lang === 'de' ? 'Human Review' : 'Human Review', + desc: lang === 'de' ? 'Experte prüft und genehmigt' : 'Expert reviews and approves', + color: 'text-cyan-400' + } + ] + + const humanInLoopFeatures = [ + lang === 'de' ? 'KI schlägt vor, Mensch entscheidet' : 'AI suggests, human decides', + lang === 'de' ? 'Kritische Entscheidungen erfordern Genehmigung' : 'Critical decisions require approval', + lang === 'de' ? 'Audit-Trail für alle Aktionen' : 'Audit trail for all actions', + lang === 'de' ? 'Compliance Officer immer in Kontrolle' : 'Compliance officer always in control' + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Autonomer Compliance-Workflow' : 'Autonomous Compliance Workflow'} + +

+

+ {lang === 'de' ? 'End-to-End: Vom Upload bis zum fertigen Report' : 'End-to-end: From upload to finished report'} +

+
+
+ +
+ {/* Workflow Steps */} +
+ +
+ {workflowSteps.map((step, idx) => ( + +
+
+ +
+
+
+ + {lang === 'de' ? 'SCHRITT' : 'STEP'} {idx + 1} + +
+

{step.title}

+

{step.desc}

+
+
+ + {/* Connection line to next step */} + {idx < workflowSteps.length - 1 && ( +
+ )} + + ))} +
+ +
+ + {/* Human-in-the-Loop */} +
+ + +
+ +

+ {lang === 'de' ? 'Human-in-the-Loop' : 'Human-in-the-Loop'} +

+
+ +
    + {humanInLoopFeatures.map((feature, idx) => ( + +
    +

    {feature}

    + + ))} +
+ + +

+ {lang === 'de' + ? 'KI automatisiert Routine, Mensch behält Kontrolle über kritische Entscheidungen' + : 'AI automates routine, human retains control over critical decisions'} +

+
+
+
+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexDevOpsSlide.tsx b/pitch-deck/components/slides/AnnexDevOpsSlide.tsx new file mode 100644 index 0000000..5b41327 --- /dev/null +++ b/pitch-deck/components/slides/AnnexDevOpsSlide.tsx @@ -0,0 +1,146 @@ +'use client' + +import { motion } from 'framer-motion' +import { GitBranch, Code, Hammer, TestTube, Rocket, ArrowRight, Container, GitMerge, Activity, Bell } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexDevOpsSlideProps { + lang: Language +} + +export default function AnnexDevOpsSlide({ lang }: AnnexDevOpsSlideProps) { + const pipelineSteps = [ + { name: 'Code', icon: GitBranch, color: 'from-blue-400 to-blue-600' }, + { name: 'Gitea', icon: Code, color: 'from-green-400 to-green-600' }, + { name: 'Woodpecker CI', icon: Hammer, color: 'from-orange-400 to-orange-600' }, + { name: 'Build', icon: Container, color: 'from-purple-400 to-purple-600' }, + { name: 'Test', icon: TestTube, color: 'from-pink-400 to-pink-600' }, + { name: 'Deploy', icon: Rocket, color: 'from-cyan-400 to-cyan-600' } + ] + + const devOpsTools = [ + { + title: 'Woodpecker CI', + icon: Hammer, + accentColor: 'from-orange-400 to-orange-600', + items: [ + lang === 'de' ? 'Open-Source CI/CD (Apache 2.0)' : 'Open-source CI/CD (Apache 2.0)', + lang === 'de' ? 'Docker-native Builds' : 'Docker-native builds', + lang === 'de' ? 'Automatische Tests bei jedem Commit' : 'Automatic testing on every commit', + lang === 'de' ? 'Build-Zeit: ~3 min' : 'Build time: ~3 min' + ] + }, + { + title: 'Docker Compose', + icon: Container, + accentColor: 'from-blue-400 to-blue-600', + items: [ + lang === 'de' ? '30+ Services orchestriert' : '30+ services orchestrated', + lang === 'de' ? 'Health Checks für alle Container' : 'Health checks for all containers', + lang === 'de' ? 'Automatischer Neustart bei Fehler' : 'Automatic restart on failure', + lang === 'de' ? 'Ressourcenlimits konfiguriert' : 'Resource limits configured' + ] + }, + { + title: 'Gitea', + icon: GitMerge, + accentColor: 'from-green-400 to-green-600', + items: [ + lang === 'de' ? 'Self-hosted Git Server' : 'Self-hosted Git server', + lang === 'de' ? 'Keine GitHub/GitLab Abhängigkeit' : 'No GitHub/GitLab dependency', + lang === 'de' ? 'Issue Tracking integriert' : 'Issue tracking integrated', + lang === 'de' ? 'Mirror zu externem Backup' : 'Mirror to external backup' + ] + }, + { + title: 'Monitoring', + icon: Activity, + accentColor: 'from-purple-400 to-purple-600', + items: [ + lang === 'de' ? 'Night Scheduler für Auto-Shutdown' : 'Night Scheduler for auto-shutdown', + lang === 'de' ? 'Container Health Monitoring' : 'Container health monitoring', + lang === 'de' ? 'Log-Aggregation' : 'Log aggregation', + lang === 'de' ? 'Alerting bei Fehlern' : 'Alerting on failures' + ] + } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'DevOps & CI/CD' : 'DevOps & CI/CD'} + +

+

+ {lang === 'de' + ? 'Automatisierte Entwicklungs- und Deployment-Pipeline' + : 'Automated development and deployment pipeline'} +

+
+
+ + {/* CI/CD Pipeline Visualization */} + +

+ {lang === 'de' ? 'CI/CD Pipeline' : 'CI/CD Pipeline'} +

+
+ {pipelineSteps.map((step, index) => ( +
+ +
+ +
+ {step.name} +
+ {index < pipelineSteps.length - 1 && ( + + )} +
+ ))} +
+
+ + {/* DevOps Tools Grid */} +
+ {devOpsTools.map((tool, index) => ( + +
+ +
+

{tool.title}

+
    + {tool.items.map((item, itemIndex) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexInfraSlide.tsx b/pitch-deck/components/slides/AnnexInfraSlide.tsx new file mode 100644 index 0000000..4f66024 --- /dev/null +++ b/pitch-deck/components/slides/AnnexInfraSlide.tsx @@ -0,0 +1,97 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Server, Container, ShieldOff, Calculator } from 'lucide-react' + +interface AnnexInfraSlideProps { + lang: Language +} + +export default function AnnexInfraSlide({ lang }: AnnexInfraSlideProps) { + const title = lang === 'de' ? 'Infrastruktur & Self-Hosting' : 'Infrastructure & Self-Hosting' + const subtitle = lang === 'de' ? 'Warum wir auf eigene Hardware setzen' : 'Why we rely on our own hardware' + + const sections = [ + { + icon: Server, + title: 'Apple Silicon Hardware', + items: [ + 'Mac Mini M2 (Starter), Mac Mini M4 Pro (Business), Mac Studio M4 Max (Enterprise)', + 'Unified Memory: 16GB / 48GB / 128GB', + 'Neural Engine for ML inference' + ] + }, + { + icon: Container, + title: 'Docker Architecture', + items: [ + '30+ Microservices in Docker Compose', + 'Automatic health monitoring', + 'Zero-downtime updates via rolling restarts' + ] + }, + { + icon: ShieldOff, + title: lang === 'de' ? 'Why No Cloud?' : 'Why No Cloud?', + items: [ + lang === 'de' ? 'Data sovereignty: No data leaves the company' : 'Data sovereignty: No data leaves the company', + lang === 'de' ? 'No recurring cloud costs (AWS/Azure)' : 'No recurring cloud costs (AWS/Azure)', + lang === 'de' ? 'BSI-TR-03161 compliance easier on-premise' : 'BSI-TR-03161 compliance easier on-premise', + lang === 'de' ? 'DSGVO Art. 28: No third-party processors' : 'DSGVO Art. 28: No third-party processors' + ] + }, + { + icon: Calculator, + title: 'Cost Comparison', + items: [ + lang === 'de' ? 'Self-Hosted: EUR 599 hardware + EUR 149/mo' : 'Self-Hosted: EUR 599 hardware + EUR 149/mo', + lang === 'de' ? 'Cloud equivalent: EUR 800-1200/mo (AWS)' : 'Cloud equivalent: EUR 800-1200/mo (AWS)', + lang === 'de' ? 'Break-even after 4-5 months' : 'Break-even after 4-5 months', + lang === 'de' ? '70% cheaper over 3 years' : '70% cheaper over 3 years' + ] + } + ] + + return ( +
+ +

+ {title} +

+

{subtitle}

+
+ +
+ {sections.map((section, idx) => { + const Icon = section.icon + return ( + +
+
+ +
+

{section.title}

+
+
    + {section.items.map((item, itemIdx) => ( +
  • + {item} +
  • + ))} +
+
+ ) + })} +
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexRAGSlide.tsx b/pitch-deck/components/slides/AnnexRAGSlide.tsx new file mode 100644 index 0000000..7eea255 --- /dev/null +++ b/pitch-deck/components/slides/AnnexRAGSlide.tsx @@ -0,0 +1,204 @@ +'use client' + +import { motion } from 'framer-motion' +import { Database, FileSearch, Layers, BarChart3, FileUp, ScanLine, Brain } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexRAGSlideProps { + lang: Language +} + +export default function AnnexRAGSlide({ lang }: AnnexRAGSlideProps) { + const pipelineSteps = [ + { + icon: FileUp, + labelDE: 'Dokument Upload', + labelEN: 'Document Upload' + }, + { + icon: ScanLine, + labelDE: 'OCR Verarbeitung', + labelEN: 'OCR Processing' + }, + { + icon: Layers, + labelDE: 'Chunking', + labelEN: 'Chunking' + }, + { + icon: Brain, + labelDE: 'Embedding', + labelEN: 'Embedding' + }, + { + icon: Database, + labelDE: 'Vector Store', + labelEN: 'Vector Store' + } + ] + + const features = [ + { + titleDE: 'Qdrant Vector DB', + titleEN: 'Qdrant Vector DB', + icon: Database, + pointsDE: [ + 'High-performance vector search', + 'Hybrid search: semantisch + keyword', + 'Filter nach Metadaten (Typ, Datum, Vorschrift)', + '99.2% Retrieval-Genauigkeit' + ], + pointsEN: [ + 'High-performance vector search', + 'Hybrid search: semantic + keyword', + 'Filters by metadata (document type, date, regulation)', + '99.2% retrieval accuracy' + ] + }, + { + titleDE: 'Embedding Pipeline', + titleEN: 'Embedding Pipeline', + icon: FileSearch, + pointsDE: [ + 'PaddleOCR für Dokumentenscanning', + 'Chunk-Größe: 512 Tokens, Overlap: 64', + 'Sentence-Transformers für Embedding', + 'Automatische Spracherkennung' + ], + pointsEN: [ + 'PaddleOCR for document scanning', + 'Chunk size: 512 tokens, overlap: 64', + 'Sentence-Transformers for embedding', + 'Automatic language detection' + ] + }, + { + titleDE: 'Hybrid Search', + titleEN: 'Hybrid Search', + icon: Layers, + pointsDE: [ + 'Kombiniert dense + sparse Vektoren', + 'BM25 für Keyword-Matching', + 'Cosine-Similarity für semantische Suche', + 'Re-Ranking für optimale Ergebnisse' + ], + pointsEN: [ + 'Combines dense + sparse vectors', + 'BM25 for keyword matching', + 'Cosine similarity for semantic search', + 'Re-ranking for optimal results' + ] + }, + { + titleDE: 'Retrieval Metrics', + titleEN: 'Retrieval Metrics', + icon: BarChart3, + pointsDE: [ + 'Precision@5: 94.3%', + 'Recall@10: 97.1%', + 'MRR: 0.89', + 'Avg. Latenz: 120ms' + ], + pointsEN: [ + 'Precision@5: 94.3%', + 'Recall@10: 97.1%', + 'MRR: 0.89', + 'Avg latency: 120ms' + ] + } + ] + + return ( +
+ {/* Header */} + +
+

+ + {lang === 'de' ? 'RAG Pipeline & Vektordatenbank' : 'RAG Pipeline & Vector Database'} + +

+

+ {lang === 'de' + ? 'Präzise Antworten durch intelligentes Retrieval' + : 'Precise answers through intelligent retrieval'} +

+
+
+ + {/* Pipeline Visualization */} + +
+
+ {pipelineSteps.map((step, index) => { + const Icon = step.icon + return ( +
+ +
+ +

+ {lang === 'de' ? step.labelDE : step.labelEN} +

+
+
+ + {index < pipelineSteps.length - 1 && ( + +
+ + )} +
+ ) + })} +
+
+ + + {/* Feature Grid */} +
+ {features.map((feature, index) => { + const Icon = feature.icon + return ( + +
+
+ +
+

+ {lang === 'de' ? feature.titleDE : feature.titleEN} +

+
+
    + {(lang === 'de' ? feature.pointsDE : feature.pointsEN).map((point, idx) => ( +
  • + + {point} +
  • + ))} +
+
+ ) + })} +
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx b/pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx new file mode 100644 index 0000000..ab5eafe --- /dev/null +++ b/pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx @@ -0,0 +1,142 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Users, Target, TrendingUp, Award } from 'lucide-react' + +interface AnnexRoadmap2027SlideProps { + lang: Language +} + +export default function AnnexRoadmap2027Slide({ lang }: AnnexRoadmap2027SlideProps) { + const quarters = [ + { + quarter: 'Q1 2027', + accent: 'from-blue-500 to-cyan-500', + items: [ + lang === 'de' ? 'Multi-Model Router Launch' : 'Multi-Model Router launch', + lang === 'de' ? 'RAG 2.0 mit Advanced Retrieval' : 'RAG 2.0 with advanced retrieval', + lang === 'de' ? '15 zahlende Kunden' : '15 paying customers', + lang === 'de' ? 'Team: 3 Personen' : 'Team: 3 people' + ] + }, + { + quarter: 'Q2 2027', + accent: 'from-indigo-500 to-blue-500', + items: [ + lang === 'de' ? 'Auto Compliance-Scan Feature' : 'Auto Compliance-Scan feature', + lang === 'de' ? 'Kunden Self-Service Portal' : 'Customer self-service portal', + lang === 'de' ? '25 zahlende Kunden' : '25 paying customers', + lang === 'de' ? 'Erster Enterprise Pilot' : 'First enterprise pilot' + ] + }, + { + quarter: 'Q3 2027', + accent: 'from-purple-500 to-indigo-500', + items: [ + lang === 'de' ? 'Echtzeit-Monitoring Dashboard' : 'Real-time monitoring dashboard', + lang === 'de' ? 'API für Partner-Integrationen' : 'API for partner integrations', + lang === 'de' ? '35 zahlende Kunden' : '35 paying customers', + lang === 'de' ? 'Team: 4 Personen' : 'Team: 4 people' + ] + }, + { + quarter: 'Q4 2027', + accent: 'from-violet-500 to-purple-500', + items: [ + lang === 'de' ? 'Series A Vorbereitung' : 'Series A preparation', + lang === 'de' ? 'Due Diligence Package bereit' : 'Due diligence package ready', + lang === 'de' ? '50+ zahlende Kunden' : '50+ paying customers', + 'ARR: EUR 90k' + ] + } + ] + + const metrics = [ + { + icon: Target, + label: lang === 'de' ? 'Zielkunden' : 'Target Customers', + value: '50+' + }, + { + icon: TrendingUp, + label: lang === 'de' ? 'Ziel ARR' : 'Target ARR', + value: 'EUR 90k' + }, + { + icon: Users, + label: lang === 'de' ? 'Team-Größe' : 'Team Size', + value: '4' + }, + { + icon: Award, + label: lang === 'de' ? 'Key Milestone' : 'Key Milestone', + value: 'Series A Ready' + } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Roadmap 2027' : 'Roadmap 2027'} + +

+

+ {lang === 'de' + ? 'Product-Market Fit & Series A Vorbereitung' + : 'Product-Market Fit & Series A Preparation'} +

+
+
+ +
+ {quarters.map((quarter, index) => ( + +
+ {quarter.quarter} +
+
    + {quarter.items.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ + +
+ {metrics.map((metric, index) => { + const Icon = metric.icon + return ( +
+ +

{metric.label}

+

{metric.value}

+
+ ) + })} +
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx b/pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx new file mode 100644 index 0000000..75cd136 --- /dev/null +++ b/pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx @@ -0,0 +1,142 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Users, Target, TrendingUp, Globe } from 'lucide-react' + +interface AnnexRoadmap2028SlideProps { + lang: Language +} + +export default function AnnexRoadmap2028Slide({ lang }: AnnexRoadmap2028SlideProps) { + const quarters = [ + { + quarter: 'Q1 2028', + accent: 'from-blue-500 to-cyan-500', + items: [ + lang === 'de' ? 'Federated Learning über Kunden' : 'Federated Learning across customers', + lang === 'de' ? 'Multi-Tenant Architektur' : 'Multi-tenant architecture', + lang === 'de' ? '80 zahlende Kunden' : '80 paying customers', + lang === 'de' ? 'Series A Abschluss (EUR 2-3M)' : 'Series A close (EUR 2-3M)' + ] + }, + { + quarter: 'Q2 2028', + accent: 'from-indigo-500 to-blue-500', + items: [ + lang === 'de' ? 'API Marketplace Launch' : 'API Marketplace launch', + lang === 'de' ? 'Partner Ökosystem Start' : 'Partner ecosystem start', + lang === 'de' ? '120 zahlende Kunden' : '120 paying customers', + lang === 'de' ? 'Team: 6 Personen' : 'Team: 6 people' + ] + }, + { + quarter: 'Q3 2028', + accent: 'from-purple-500 to-indigo-500', + items: [ + lang === 'de' ? 'Enterprise Tier Launch' : 'Enterprise tier launch', + lang === 'de' ? 'DACH Expansion (Österreich, Schweiz)' : 'DACH expansion (Austria, Switzerland)', + lang === 'de' ? '160 zahlende Kunden' : '160 paying customers', + lang === 'de' ? 'Team: 7 Personen' : 'Team: 7 people' + ] + }, + { + quarter: 'Q4 2028', + accent: 'from-violet-500 to-purple-500', + items: [ + lang === 'de' ? 'Volle DACH Abdeckung' : 'Full DACH coverage', + lang === 'de' ? '200+ zahlende Kunden' : '200+ paying customers', + 'ARR: EUR 360k', + lang === 'de' ? 'Team: 8 Personen' : 'Team: 8 people' + ] + } + ] + + const metrics = [ + { + icon: Target, + label: lang === 'de' ? 'Zielkunden' : 'Target Customers', + value: '200+' + }, + { + icon: TrendingUp, + label: lang === 'de' ? 'Ziel ARR' : 'Target ARR', + value: 'EUR 360k' + }, + { + icon: Users, + label: lang === 'de' ? 'Team-Größe' : 'Team Size', + value: '8' + }, + { + icon: Globe, + label: lang === 'de' ? 'Key Milestone' : 'Key Milestone', + value: lang === 'de' ? 'DACH Expansion' : 'DACH Expansion' + } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Roadmap 2028' : 'Roadmap 2028'} + +

+

+ {lang === 'de' + ? 'Enterprise Scale & Series A' + : 'Enterprise Scale & Series A'} +

+
+
+ +
+ {quarters.map((quarter, index) => ( + +
+ {quarter.quarter} +
+
    + {quarter.items.map((item, i) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ + +
+ {metrics.map((metric, index) => { + const Icon = metric.icon + return ( +
+ +

{metric.label}

+

{metric.value}

+
+ ) + })} +
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexSecuritySlide.tsx b/pitch-deck/components/slides/AnnexSecuritySlide.tsx new file mode 100644 index 0000000..ffcd614 --- /dev/null +++ b/pitch-deck/components/slides/AnnexSecuritySlide.tsx @@ -0,0 +1,130 @@ +'use client' + +import { motion } from 'framer-motion' +import { Shield, Lock, ShieldCheck, Search, FileCode, Key, FileText } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexSecuritySlideProps { + lang: Language +} + +export default function AnnexSecuritySlide({ lang }: AnnexSecuritySlideProps) { + const securityFeatures = [ + { + icon: Shield, + title: lang === 'de' ? 'BSI & Compliance' : 'BSI & Compliance', + accentColor: 'from-blue-400 to-blue-600', + items: [ + lang === 'de' ? 'BSI-TR-03161 zertifizierte Architektur' : 'BSI-TR-03161 certified architecture', + lang === 'de' ? 'DSGVO Art. 32 TOM implementiert' : 'DSGVO Art. 32 TOM implemented', + lang === 'de' ? 'Regelmäßige Penetrationstests' : 'Regular penetration testing', + lang === 'de' ? 'Sicherheits-Audit-Trail' : 'Security audit trail' + ] + }, + { + icon: Lock, + title: lang === 'de' ? 'Verschlüsselung & Vault' : 'Encryption & Vault', + accentColor: 'from-purple-400 to-purple-600', + items: [ + lang === 'de' ? 'E2E-Verschlüsselung für Daten in Transit (TLS 1.3)' : 'E2E encryption for all data in transit (TLS 1.3)', + lang === 'de' ? 'AES-256 Verschlüsselung im Ruhezustand' : 'AES-256 encryption at rest', + lang === 'de' ? 'HashiCorp Vault für Secret Management' : 'HashiCorp Vault for secret management', + lang === 'de' ? 'Automatische Zertifikatsrotation' : 'Automatic certificate rotation' + ] + }, + { + icon: ShieldCheck, + title: lang === 'de' ? 'Zero Trust' : 'Zero Trust', + accentColor: 'from-green-400 to-green-600', + items: [ + lang === 'de' ? 'Kein implizites Vertrauen, alles verifizieren' : 'No implicit trust, verify everything', + lang === 'de' ? 'JWT-basierte Authentifizierung' : 'JWT-based authentication', + lang === 'de' ? 'RBAC mit minimalen Rechten' : 'RBAC with least privilege', + lang === 'de' ? 'Netzwerksegmentierung via Docker' : 'Network segmentation via Docker' + ] + } + ] + + const securityTools = [ + { name: 'Trivy', description: lang === 'de' ? 'Container Scanning' : 'Container Scanning', icon: Search }, + { name: 'Semgrep', description: 'SAST', icon: FileCode }, + { name: 'Gitleaks', description: lang === 'de' ? 'Secret Detection' : 'Secret Detection', icon: Key }, + { name: 'SBOM', description: lang === 'de' ? 'Software Bill of Materials' : 'Software Bill of Materials', icon: FileText } + ] + + return ( +
+ +
+

+ + {lang === 'de' ? 'Sicherheitsarchitektur' : 'Security Architecture'} + +

+

+ {lang === 'de' + ? 'Enterprise-Grade Sicherheit auf eigener Hardware' + : 'Enterprise-grade security on own hardware'} +

+
+
+ + {/* Security Features Grid */} +
+ {securityFeatures.map((feature, index) => ( + +
+ +
+

{feature.title}

+
    + {feature.items.map((item, itemIndex) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ + {/* Security Tools Bar */} + +

+ {lang === 'de' ? 'Sicherheitstools' : 'Security Tools'} +

+
+ {securityTools.map((tool, index) => ( + + +
+ {tool.name} + {tool.description} +
+
+ ))} +
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx b/pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx new file mode 100644 index 0000000..eabb550 --- /dev/null +++ b/pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx @@ -0,0 +1,181 @@ +'use client' + +import { motion } from 'framer-motion' +import { Check, X, Minus } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexUSPComparisonSlideProps { + lang: Language +} + +const comparisonData = [ + { + featureDE: 'Self-Hosted', + featureEN: 'Self-Hosted', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'no', + heydata: 'no' + }, + { + featureDE: 'Eigene KI', + featureEN: 'Own AI', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'partial', + heydata: 'no' + }, + { + featureDE: 'Autonomer Support', + featureEN: 'Autonomous Support', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'no', + heydata: 'no' + }, + { + featureDE: 'DSGVO', + featureEN: 'GDPR', + breakpilot: 'yes', + proliance: 'yes', + dataguard: 'yes', + heydata: 'yes' + }, + { + featureDE: 'AI Act', + featureEN: 'AI Act', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'partial', + heydata: 'no' + }, + { + featureDE: 'NIS2', + featureEN: 'NIS2', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'no', + heydata: 'no' + }, + { + featureDE: 'Hardware inkl.', + featureEN: 'Hardware incl.', + breakpilot: 'yes', + proliance: 'no', + dataguard: 'no', + heydata: 'no' + }, + { + featureDE: 'Preis/Monat', + featureEN: 'Price/Month', + breakpilot: 'ab EUR 149', + proliance: 'ab EUR 299', + dataguard: 'ab EUR 499', + heydata: 'ab EUR 199' + } +] + +const StatusIcon = ({ status }: { status: string }) => { + if (status === 'yes') { + return + } else if (status === 'no') { + return + } else if (status === 'partial') { + return + } + return {status} +} + +export default function AnnexUSPComparisonSlide({ lang }: AnnexUSPComparisonSlideProps) { + return ( +
+ +
+

+ + {lang === 'de' ? 'Detailvergleich Wettbewerb' : 'Detailed Competitor Comparison'} + +

+

+ {lang === 'de' ? 'BreakPilot vs. etablierte Anbieter' : 'BreakPilot vs. established providers'} +

+
+
+ + +
+ + + + + + + + + + + + {comparisonData.map((row, index) => ( + + + + + + + + ))} + +
+
+ BreakPilot +
+
ProlianceDataGuardheyData
+ {lang === 'de' ? row.featureDE : row.featureEN} + +
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + +

+ {lang === 'de' + ? 'Stand: Februar 2026. Preise und Features der Wettbewerber koennen variieren.' + : 'As of: February 2026. Competitor prices and features may vary.'} +

+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexUSPMoatSlide.tsx b/pitch-deck/components/slides/AnnexUSPMoatSlide.tsx new file mode 100644 index 0000000..5819cb4 --- /dev/null +++ b/pitch-deck/components/slides/AnnexUSPMoatSlide.tsx @@ -0,0 +1,127 @@ +'use client' + +import { motion } from 'framer-motion' +import { Lock, RefreshCw, Network, RotateCw, Clock } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexUSPMoatSlideProps { + lang: Language +} + +const moats = [ + { + icon: Lock, + gradient: 'from-blue-500 to-cyan-500', + titleDE: 'Hardware Lock-In', + titleEN: 'Hardware Lock-In', + descDE: 'Physische Hardware beim Kunden. Wechsel bedeutet Hardware-Austausch und Datenmigration.', + descEN: 'Physical hardware at customer site. Switching means hardware replacement and data migration.' + }, + { + icon: RefreshCw, + gradient: 'from-purple-500 to-pink-500', + titleDE: 'Switching Costs', + titleEN: 'Switching Costs', + descDE: 'Compliance-Dokumentation, trainierte KI-Modelle und Audit-Historie sind nicht portabel.', + descEN: 'Compliance documentation, trained AI models and audit history are not portable.' + }, + { + icon: Network, + gradient: 'from-green-500 to-emerald-500', + titleDE: 'Network Effects', + titleEN: 'Network Effects', + descDE: 'Jeder Kunde verbessert die KI. Shared learnings ueber anonymisierte Compliance-Patterns.', + descEN: 'Every customer improves the AI. Shared learnings via anonymized compliance patterns.' + }, + { + icon: RotateCw, + gradient: 'from-amber-500 to-orange-500', + titleDE: 'Data Flywheel', + titleEN: 'Data Flywheel', + descDE: 'Mehr Daten → bessere KI → bessere Compliance → mehr Kunden → mehr Daten.', + descEN: 'More data → better AI → better compliance → more customers → more data.' + }, + { + icon: Clock, + gradient: 'from-pink-500 to-rose-500', + titleDE: 'Time Advantage', + titleEN: 'Time Advantage', + descDE: '18 Monate Vorsprung. Erster Self-Hosted Compliance-AI Anbieter im DACH-Raum.', + descEN: '18 months head start. First self-hosted compliance AI provider in DACH region.' + } +] + +export default function AnnexUSPMoatSlide({ lang }: AnnexUSPMoatSlideProps) { + return ( +
+ +
+

+ + {lang === 'de' ? 'Verteidigbare Marktposition' : 'Defensible Market Position'} + +

+

+ {lang === 'de' + ? 'Fuenf Verteidigungslinien gegen Wettbewerber' + : 'Five lines of defense against competitors'} +

+
+
+ +
+ {moats.map((moat, index) => { + const Icon = moat.icon + return ( + +
+
+
+ + {String(index + 1).padStart(2, '0')} + +
+ +
+
+ +
+

+ {lang === 'de' ? moat.titleDE : moat.titleEN} +

+

+ {lang === 'de' ? moat.descDE : moat.descEN} +

+
+ +
+ + ) + })} +
+ + +
+

+ {lang === 'de' + ? 'Kombination aus physischen und digitalen Barriers macht Marktposition langfristig verteidigbar' + : 'Combination of physical and digital barriers makes market position defensible long-term'} +

+
+
+
+ ) +} diff --git a/pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx b/pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx new file mode 100644 index 0000000..6cbc475 --- /dev/null +++ b/pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx @@ -0,0 +1,129 @@ +'use client' + +import { motion } from 'framer-motion' +import { Server, Brain, Shield, Layers, Bot } from 'lucide-react' +import { Language } from '@/lib/types' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' + +interface AnnexUSPOverviewSlideProps { + lang: Language +} + +const usps = [ + { + icon: Server, + gradient: 'from-blue-500 to-cyan-500', + titleDE: 'Self-Hosted', + titleEN: 'Self-Hosted', + descDE: 'Volle Datensouveraenitaet. Kein Byte verlaesst das Unternehmen. Hardware steht im eigenen Serverraum.', + descEN: 'Full data sovereignty. No byte leaves the company. Hardware sits in your own server room.' + }, + { + icon: Brain, + gradient: 'from-purple-500 to-pink-500', + titleDE: 'AI-First', + titleEN: 'AI-First', + descDE: 'Alles was durch KI loesbar ist, wird durch KI geloest. Kein klassischer Support, minimales Personal.', + descEN: 'Everything solvable by AI is solved by AI. No traditional support, minimal personnel.' + }, + { + icon: Shield, + gradient: 'from-green-500 to-emerald-500', + titleDE: 'Hardware-Moat', + titleEN: 'Hardware-Moat', + descDE: 'Apple Hardware als physische Markteintrittsbarriere. Wechselkosten machen Lock-in positiv.', + descEN: 'Apple hardware as physical market entry barrier. Switching costs create positive lock-in.' + }, + { + icon: Layers, + gradient: 'from-amber-500 to-orange-500', + titleDE: 'All-in-One', + titleEN: 'All-in-One', + descDE: 'DSGVO + AI Act + NIS2 in einer Loesung. Kein Tool-Dschungel, eine Plattform fuer alles.', + descEN: 'GDPR + AI Act + NIS2 in one solution. No tool jungle, one platform for everything.' + }, + { + icon: Bot, + gradient: 'from-pink-500 to-rose-500', + titleDE: 'Autonomous Support', + titleEN: 'Autonomous Support', + descDE: '24/7 KI-Support beantwortet Fragen, aendert Dokumente, bereitet Audits vor. Ohne menschliches Eingreifen.', + descEN: '24/7 AI support answers questions, modifies documents, prepares audits. Without human intervention.' + } +] + +export default function AnnexUSPOverviewSlide({ lang }: AnnexUSPOverviewSlideProps) { + return ( +
+ +
+

+ + {lang === 'de' ? 'Unsere 5 USPs' : 'Our 5 USPs'} + +

+

+ {lang === 'de' ? 'Was uns einzigartig macht' : 'What makes us unique'} +

+
+
+ +
+ {usps.slice(0, 3).map((usp, index) => { + const Icon = usp.icon + return ( + +
+
+
+ +
+

+ {lang === 'de' ? usp.titleDE : usp.titleEN} +

+

+ {lang === 'de' ? usp.descDE : usp.descEN} +

+
+ + ) + })} +
+ +
+ {usps.slice(3, 5).map((usp, index) => { + const Icon = usp.icon + return ( + +
+
+
+ +
+

+ {lang === 'de' ? usp.titleDE : usp.titleEN} +

+

+ {lang === 'de' ? usp.descDE : usp.descEN} +

+
+ + ) + })} +
+
+ ) +} diff --git a/pitch-deck/components/slides/AppendixSlide.tsx b/pitch-deck/components/slides/AppendixSlide.tsx new file mode 100644 index 0000000..542d29d --- /dev/null +++ b/pitch-deck/components/slides/AppendixSlide.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Language } from '@/lib/types'; +import GradientText from '../ui/GradientText'; +import FadeInView from '../ui/FadeInView'; + +interface AppendixSlideProps { + lang: Language; +} + +export default function AppendixSlide({ lang }: AppendixSlideProps) { + const content = { + de: { + title: 'Appendix', + subtitle: 'Deep Dive — Technologie, Compliance & Strategie', + detailInfo: '14 Detailfolien für Investoren', + }, + en: { + title: 'Appendix', + subtitle: 'Deep Dive — Technology, Compliance & Strategy', + detailInfo: '14 detail slides for investors', + }, + }; + + const t = content[lang]; + + return ( +
+ + +

+ {t.title} +

+
+ + + {t.subtitle} + + + + + + {t.detailInfo} + +
+
+ ); +} diff --git a/pitch-deck/components/slides/CompetitionSlide.tsx b/pitch-deck/components/slides/CompetitionSlide.tsx index 363ae5a..2a943e4 100644 --- a/pitch-deck/components/slides/CompetitionSlide.tsx +++ b/pitch-deck/components/slides/CompetitionSlide.tsx @@ -7,6 +7,7 @@ import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import FeatureMatrix from '../ui/FeatureMatrix' import GlassCard from '../ui/GlassCard' +import BrandName from '../ui/BrandName' interface CompetitionSlideProps { lang: Language @@ -59,7 +60,7 @@ export default function CompetitionSlide({ lang, features, competitors }: Compet

- {lang === 'de' ? 'Integrierte Security & Developer Tools — nur bei ComplAI' : 'Integrated Security & Developer Tools — ComplAI only'} + {lang === 'de' ? <>Integrierte Security & Developer Tools — nur bei : <>Integrated Security & Developer Tools — only}

{secFeats.map((feat, idx) => { diff --git a/pitch-deck/components/slides/CoverSlide.tsx b/pitch-deck/components/slides/CoverSlide.tsx index 84e2dd1..b4e5570 100644 --- a/pitch-deck/components/slides/CoverSlide.tsx +++ b/pitch-deck/components/slides/CoverSlide.tsx @@ -5,6 +5,7 @@ import { Language } from '@/lib/types' import { t } from '@/lib/i18n' import { ArrowRight } from 'lucide-react' import GradientText from '../ui/GradientText' +import BrandName from '../ui/BrandName' interface CoverSlideProps { lang: Language @@ -43,9 +44,13 @@ export default function CoverSlide({ lang, onNext }: CoverSlideProps) { className="text-5xl md:text-7xl font-bold mb-4 tracking-tight" > BreakPilot{' '} - - ComplAI - + + + {/* Tagline */} diff --git a/pitch-deck/components/slides/FinancialsSlide.tsx b/pitch-deck/components/slides/FinancialsSlide.tsx index c1f52af..67a2ddd 100644 --- a/pitch-deck/components/slides/FinancialsSlide.tsx +++ b/pitch-deck/components/slides/FinancialsSlide.tsx @@ -1,92 +1,312 @@ 'use client' import { useState } from 'react' -import { Language, PitchFinancial } from '@/lib/types' -import { t, formatEur } from '@/lib/i18n' +import { Language } from '@/lib/types' +import { t } from '@/lib/i18n' +import { useFinancialModel } from '@/lib/hooks/useFinancialModel' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import FinancialChart from '../ui/FinancialChart' import FinancialSliders from '../ui/FinancialSliders' -import GlassCard from '../ui/GlassCard' -import AnimatedCounter from '../ui/AnimatedCounter' +import KPICard from '../ui/KPICard' +import RunwayGauge from '../ui/RunwayGauge' +import WaterfallChart from '../ui/WaterfallChart' +import UnitEconomicsCards from '../ui/UnitEconomicsCards' +import ScenarioSwitcher from '../ui/ScenarioSwitcher' +import AnnualPLTable, { AccountingStandard } from '../ui/AnnualPLTable' +import AnnualCashflowChart from '../ui/AnnualCashflowChart' + +type FinTab = 'overview' | 'guv' | 'cashflow' interface FinancialsSlideProps { lang: Language - financials: PitchFinancial[] } -export default function FinancialsSlide({ lang, financials }: FinancialsSlideProps) { +export default function FinancialsSlide({ lang }: FinancialsSlideProps) { const i = t(lang) - const [growthRate, setGrowthRate] = useState(100) - const [churnRate, setChurnRate] = useState(5) - const [arpu, setArpu] = useState(500) + const fm = useFinancialModel() + const [activeTab, setActiveTab] = useState('overview') + const [accountingStandard, setAccountingStandard] = useState('hgb') + const de = lang === 'de' - const growthMultiplier = growthRate / 100 - const lastYear = financials[financials.length - 1] + const activeResults = fm.activeResults + const summary = activeResults?.summary + const lastResult = activeResults?.results[activeResults.results.length - 1] + + // Build scenario color map + const scenarioColors: Record = {} + fm.scenarios.forEach(s => { scenarioColors[s.id] = s.color }) + + // Build compare results (exclude active scenario) + const compareResults = new Map( + Array.from(fm.results.entries()).filter(([id]) => id !== fm.activeScenarioId) + ) + + // Initial funding from assumptions + const initialFunding = (fm.activeScenario?.assumptions.find(a => a.key === 'initial_funding')?.value as number) || 200000 + + const tabs: { id: FinTab; label: string }[] = [ + { id: 'overview', label: de ? 'Uebersicht' : 'Overview' }, + { id: 'guv', label: de ? 'GuV (Jahres)' : 'P&L (Annual)' }, + { id: 'cashflow', label: de ? 'Cashflow & Finanzbedarf' : 'Cashflow & Funding' }, + ] + + if (fm.loading) { + return ( +
+
+
+ ) + } return ( -
- -

+
+ +

{i.financials.title}

-

{i.financials.subtitle}

+

{i.financials.subtitle}

- {/* Key Numbers */} -
- -

{i.financials.arr} 2030

-

- -

-
- -

{i.financials.customers} 2030

-

- -

-
- -

{i.financials.employees} 2030

-

{lastYear?.employees_count || 0}

-
- -

{i.financials.mrr} 2030

-

- -

-
+ {/* Hero KPI Cards */} +
+ + + + = 3 ? 'up' : 'down'} + color="#a855f7" + delay={0.25} + />
-
- {/* Chart */} - - - - - + {/* Tab Navigation + Accounting Standard Toggle */} +
+
+ {tabs.map((tab) => ( + + ))} +
- {/* Sliders */} - - - + {/* HGB / US GAAP Toggle — only visible on GuV and Cashflow tabs */} + {(activeTab === 'guv' || activeTab === 'cashflow') && ( +
+ + +
+ )} +
+ + {/* Main content: 3-column layout */} +
+ {/* Left: Charts (8 columns) */} +
+ + {/* TAB: Overview — monatlicher Chart + Waterfall + Unit Economics */} + {activeTab === 'overview' && ( + <> + +
+
+

+ {de ? 'Umsatz vs. Kosten (60 Monate)' : 'Revenue vs. Costs (60 months)'} +

+
+ {de ? 'Umsatz' : 'Revenue'} + {de ? 'Kosten' : 'Costs'} + {de ? 'Kunden' : 'Customers'} +
+
+ +
+
+ +
+ +
+

+ {de ? 'Cash-Flow (Quartal)' : 'Cash Flow (Quarterly)'} +

+ {activeResults && } +
+
+ + +
+
+ +
+ {lastResult && ( + a.key === 'churn_rate_monthly')?.value as number || 3} + lang={lang} + /> + )} +
+
+
+ + )} + + {/* TAB: GuV — Annual P&L Table */} + {activeTab === 'guv' && activeResults && ( + +
+
+

+ {accountingStandard === 'hgb' + ? (de ? 'Gewinn- und Verlustrechnung (HGB, 5 Jahre)' : 'Profit & Loss Statement (HGB, 5 Years)') + : (de ? 'Gewinn- und Verlustrechnung (US GAAP, 5 Jahre)' : 'Profit & Loss Statement (US GAAP, 5 Years)') + } +

+

+ {de ? 'Alle Werte in EUR' : 'All values in EUR'} +

+
+ +
+
+ )} + + {/* TAB: Cashflow & Finanzbedarf */} + {activeTab === 'cashflow' && activeResults && ( + +
+

+ {accountingStandard === 'hgb' + ? (de ? 'Kapitalflussrechnung (HGB)' : 'Cash Flow Statement (HGB)') + : (de ? 'Cash Flow Statement (US GAAP)' : 'Cash Flow Statement (US GAAP)') + } +

+ +
+
+ )} +
+ + {/* Right: Controls (4 columns) */} +
+ {/* Scenario Switcher */} + +
+ { + fm.setActiveScenarioId(id) + }} + onToggleCompare={() => { + if (!fm.compareMode) { + fm.computeAll() + } + fm.setCompareMode(!fm.compareMode) + }} + lang={lang} + /> +
+
+ + {/* Assumption Sliders */} + +
+

+ {i.financials.adjustAssumptions} +

+ {fm.activeScenario && ( + { + if (fm.activeScenarioId) { + fm.updateAssumption(fm.activeScenarioId, key, value) + } + }} + lang={lang} + /> + )} + {fm.computing && ( +
+
+ {de ? 'Berechne...' : 'Computing...'} +
+ )} +
+ +
) diff --git a/pitch-deck/components/slides/SolutionSlide.tsx b/pitch-deck/components/slides/SolutionSlide.tsx index d8408dd..7374deb 100644 --- a/pitch-deck/components/slides/SolutionSlide.tsx +++ b/pitch-deck/components/slides/SolutionSlide.tsx @@ -7,6 +7,7 @@ import { Server, ShieldCheck, Bot } from 'lucide-react' import GlassCard from '../ui/GlassCard' import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' +import BrandName from '../ui/BrandName' interface SolutionSlideProps { lang: Language @@ -24,7 +25,9 @@ export default function SolutionSlide({ lang }: SolutionSlideProps) {

{i.solution.title}

-

{i.solution.subtitle}

+

+ — {lang === 'de' ? 'Compliance auf Autopilot' : 'Compliance on Autopilot'} +

diff --git a/pitch-deck/components/slides/TechnologySlide.tsx b/pitch-deck/components/slides/TechnologySlide.tsx new file mode 100644 index 0000000..3ee89a0 --- /dev/null +++ b/pitch-deck/components/slides/TechnologySlide.tsx @@ -0,0 +1,125 @@ +'use client' + +import { motion } from 'framer-motion' +import { Language } from '@/lib/types' +import { t } from '@/lib/i18n' +import GradientText from '../ui/GradientText' +import FadeInView from '../ui/FadeInView' +import { Cpu, Brain, Server, Shield, Database } from 'lucide-react' + +interface TechnologySlideProps { + lang: Language +} + +const phaseColors = [ + 'from-blue-500/20 to-blue-600/10 border-blue-500/30', + 'from-indigo-500/20 to-indigo-600/10 border-indigo-500/30', + 'from-purple-500/20 to-purple-600/10 border-purple-500/30', + 'from-violet-500/20 to-violet-600/10 border-violet-500/30', + 'from-fuchsia-500/20 to-fuchsia-600/10 border-fuchsia-500/30', +] + +const phaseAccents = [ + 'text-blue-400', + 'text-indigo-400', + 'text-purple-400', + 'text-violet-400', + 'text-fuchsia-400', +] + +const layerIcons = [Cpu, Brain, Server, Shield, Database] +const layerColors = [ + 'text-blue-400 bg-blue-500/10 border-blue-500/20', + 'text-purple-400 bg-purple-500/10 border-purple-500/20', + 'text-green-400 bg-green-500/10 border-green-500/20', + 'text-red-400 bg-red-500/10 border-red-500/20', + 'text-amber-400 bg-amber-500/10 border-amber-500/20', +] + +export default function TechnologySlide({ lang }: TechnologySlideProps) { + const i = t(lang) as any + + return ( +
+ +

+ {i.technology.title} +

+

{i.technology.subtitle}

+
+ + {/* Timeline Section */} + +
+

+ {i.technology.timelineTitle} +

+ + {/* Timeline Line */} +
+
+ +
+ {i.technology.phases.map((phase: any, idx: number) => ( + + {/* Dot on timeline */} +
+
+
+ + {/* Phase Card */} +
+

{phase.year}

+

{phase.phase}

+
+ {phase.techs.map((tech: string, i: number) => ( +

+ {tech} +

+ ))} +
+
+ + ))} +
+
+
+ + + {/* Tech Stack Layers */} + +

+ {i.technology.stackTitle} +

+
+ {i.technology.layers.map((layer: any, idx: number) => { + const Icon = layerIcons[idx] + return ( + +
+ +
+
+

{layer.name}

+

{layer.techs}

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/pitch-deck/components/slides/TheAskSlide.tsx b/pitch-deck/components/slides/TheAskSlide.tsx index 19ae88c..2e4340f 100644 --- a/pitch-deck/components/slides/TheAskSlide.tsx +++ b/pitch-deck/components/slides/TheAskSlide.tsx @@ -7,7 +7,7 @@ import GradientText from '../ui/GradientText' import FadeInView from '../ui/FadeInView' import AnimatedCounter from '../ui/AnimatedCounter' import GlassCard from '../ui/GlassCard' -import { Target, Calendar, FileText } from 'lucide-react' +import { Target, Calendar, FileText, Building2, Users, Landmark, TrendingUp } from 'lucide-react' import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts' interface TheAskSlideProps { @@ -15,7 +15,50 @@ interface TheAskSlideProps { funding: PitchFunding } -const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24'] +const COLORS = ['#6366f1', '#a78bfa', '#60a5fa', '#34d399', '#fbbf24', '#f87171'] + +const FUNDING_TIMELINE = [ + { + month: 'Aug 2026', + amount: 25000, + icon: Building2, + color: '#6366f1', + label_de: 'Stammkapital', + label_en: 'Share Capital', + desc_de: 'GmbH-Gruendung', + desc_en: 'GmbH Founding', + }, + { + month: 'Sep 2026', + amount: 25000, + icon: Users, + color: '#a78bfa', + label_de: 'Angel-Runde', + label_en: 'Angel Round', + desc_de: '5% Anteile', + desc_en: '5% Equity', + }, + { + month: 'Okt 2026', + amount: 200000, + icon: Landmark, + color: '#60a5fa', + label_de: 'Wandeldarlehen', + label_en: 'Convertible Note', + desc_de: '40k Investor + 160k L-Bank', + desc_en: '40k Investor + 160k L-Bank', + }, + { + month: 'Jul 2027', + amount: 1000000, + icon: TrendingUp, + color: '#34d399', + label_de: 'Series A', + label_en: 'Series A', + desc_de: 'Skalierungskapital', + desc_en: 'Growth Capital', + }, +] export default function TheAskSlide({ lang, funding }: TheAskSlideProps) { const i = t(lang) @@ -26,9 +69,11 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) { value: item.percentage, })) + const totalFunding = 1250000 + return (
- +

{i.theAsk.title}

@@ -36,53 +81,93 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
{/* Main Number */} - + -

- - EUR +

+ + EUR +

+

+ {lang === 'de' ? 'Gestaffelte Finanzierung 2026-2027' : 'Staged Funding 2026-2027'}

- {/* Details */} -
- - + {/* Funding Timeline */} + +
+ {FUNDING_TIMELINE.map((event, idx) => { + const Icon = event.icon + return ( + + + +

{event.month}

+

+ {event.amount >= 1000000 + ? `${(event.amount / 1000000).toFixed(0)}M` + : `${(event.amount / 1000).toFixed(0)}k`} +

+

+ {lang === 'de' ? event.label_de : event.label_en} +

+

+ {lang === 'de' ? event.desc_de : event.desc_en} +

+
+
+ ) + })} +
+
+ + {/* Details Row */} +
+ +

{i.theAsk.instrument}

-

{funding?.instrument || 'SAFE'}

+

{funding?.instrument || 'Stammkapital + Wandeldarlehen + Equity'}

- - + +

{i.theAsk.targetDate}

-

Q3 2026

+

+ {lang === 'de' ? 'Aug 2026 — Jul 2027' : 'Aug 2026 — Jul 2027'} +

- - + +

{lang === 'de' ? 'Runway' : 'Runway'}

-

18 {lang === 'de' ? 'Monate' : 'Months'}

+

36+ {lang === 'de' ? 'Monate' : 'Months'}

{/* Use of Funds */} - - -

{i.theAsk.useOfFunds}

-
+ + +

+ {i.theAsk.useOfFunds} (1,25 Mio. EUR) +

+
{/* Pie Chart */} -
+
@@ -96,7 +181,7 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) { border: '1px solid rgba(255,255,255,0.1)', borderRadius: 8, color: '#fff', - fontSize: 13, + fontSize: 12, }} formatter={(value: number) => `${value}%`} /> @@ -105,16 +190,16 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
{/* Legend */} -
+
{useOfFunds.map((item, idx) => (
-
+
{lang === 'de' ? item.label_de : item.label_en} {item.percentage}% - {((funding.amount_eur * item.percentage) / 100).toLocaleString('de-DE')} EUR + {((totalFunding * item.percentage) / 100).toLocaleString('de-DE')} EUR
))} diff --git a/pitch-deck/components/ui/AnnualCashflowChart.tsx b/pitch-deck/components/ui/AnnualCashflowChart.tsx new file mode 100644 index 0000000..5e786fd --- /dev/null +++ b/pitch-deck/components/ui/AnnualCashflowChart.tsx @@ -0,0 +1,205 @@ +'use client' + +import { FMResult } from '@/lib/types' +import { AccountingStandard } from './AnnualPLTable' +import { + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + ReferenceLine, + Line, + ComposedChart, + Cell, +} from 'recharts' + +interface AnnualCashflowChartProps { + results: FMResult[] + initialFunding: number + lang: 'de' | 'en' + standard?: AccountingStandard +} + +interface AnnualCFRow { + year: string + revenue: number + costs: number + netCashflow: number + cashBalance: number + cumulativeFundingNeed: number + // HGB specific + operatingCF?: number + investingCF?: number + financingCF?: number +} + +export default function AnnualCashflowChart({ results, initialFunding, lang, standard = 'hgb' }: AnnualCashflowChartProps) { + const de = lang === 'de' + + // Aggregate into yearly + const yearMap = new Map() + for (const r of results) { + if (!yearMap.has(r.year)) yearMap.set(r.year, []) + yearMap.get(r.year)!.push(r) + } + + let cumulativeNeed = 0 + const data: AnnualCFRow[] = Array.from(yearMap.entries()).map(([year, months]) => { + const revenue = months.reduce((s, m) => s + m.revenue_eur, 0) + const costs = months.reduce((s, m) => s + m.total_costs_eur, 0) + const netIncome = months.reduce((s, m) => s + (m.net_income_eur || m.revenue_eur - m.total_costs_eur), 0) + const depreciation = months.reduce((s, m) => s + (m.depreciation_eur || 0), 0) + const cogs = months.reduce((s, m) => s + m.cogs_eur, 0) + const lastMonth = months[months.length - 1] + + const netCF = netIncome + if (netCF < 0) cumulativeNeed += Math.abs(netCF) + + // Operating CF = Net Income + Depreciation (non-cash add-back) + const operatingCF = netIncome + depreciation + // Investing CF = Hardware COGS (approximation for CapEx) + const investingCF = -cogs + // Financing CF = 0 for now (no debt/equity events modeled) + const financingCF = 0 + + return { + year: year.toString(), + revenue: Math.round(revenue), + costs: Math.round(costs), + netCashflow: Math.round(netCF), + cashBalance: Math.round(lastMonth.cash_balance_eur), + cumulativeFundingNeed: Math.round(cumulativeNeed), + operatingCF: Math.round(operatingCF), + investingCF: Math.round(investingCF), + financingCF: Math.round(financingCF), + } + }) + + const formatValue = (value: number) => { + if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` + if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(0)}k` + return value.toString() + } + + // Calculate total funding needed beyond initial funding + const totalFundingGap = Math.max(0, cumulativeNeed - initialFunding) + + const isUSGAAP = standard === 'usgaap' + + return ( +
+ {/* Summary Cards */} +
+
+

+ {de ? 'Startkapital' : 'Initial Funding'} +

+

{formatValue(initialFunding)} EUR

+
+
+

+ {de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need'} +

+

{formatValue(cumulativeNeed)} EUR

+
+
+

+ {de ? 'Finanzierungsluecke' : 'Funding Gap'} +

+

0 ? 'text-red-400' : 'text-emerald-400'}`}> + {totalFundingGap > 0 ? formatValue(totalFundingGap) + ' EUR' : (de ? 'Gedeckt' : 'Covered')} +

+
+
+ + {/* Chart */} +
+ + + + + { + const labels: Record = isUSGAAP + ? { + netCashflow: 'Net Cash Flow', + cashBalance: 'Cash Balance', + cumulativeFundingNeed: 'Cum. Funding Need', + } + : { + netCashflow: de ? 'Netto-Cashflow' : 'Net Cash Flow', + cashBalance: de ? 'Cash-Bestand' : 'Cash Balance', + cumulativeFundingNeed: de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need', + } + return [formatValue(value) + ' EUR', labels[name] || name] + }} + /> + + + {/* Net Cashflow Bars */} + + {data.map((entry, i) => ( + = 0 ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.6)'} + /> + ))} + + + {/* Cash Balance Line */} + + + {/* Cumulative Funding Need Line */} + + + +
+ + {/* Legend */} +
+ + + + {isUSGAAP ? 'Net Cash Flow' : (de ? 'Netto-Cashflow' : 'Net Cash Flow')} + + + + {isUSGAAP ? 'Cash Balance' : (de ? 'Cash-Bestand' : 'Cash Balance')} + + + + {isUSGAAP ? 'Cum. Funding Need' : (de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need')} + +
+
+ ) +} diff --git a/pitch-deck/components/ui/AnnualPLTable.tsx b/pitch-deck/components/ui/AnnualPLTable.tsx new file mode 100644 index 0000000..c1d70c8 --- /dev/null +++ b/pitch-deck/components/ui/AnnualPLTable.tsx @@ -0,0 +1,232 @@ +'use client' + +import { motion } from 'framer-motion' +import { FMResult } from '@/lib/types' + +export type AccountingStandard = 'hgb' | 'usgaap' + +interface AnnualPLTableProps { + results: FMResult[] + lang: 'de' | 'en' + standard: AccountingStandard +} + +interface AnnualRow { + year: number + revenue: number + cogs: number + grossProfit: number + grossMarginPct: number + personnel: number + marketing: number + infra: number + totalOpex: number + ebitda: number + ebitdaMarginPct: number + customers: number + employees: number + // Detail costs + adminCosts: number + officeCosts: number + travelCosts: number + softwareLicenses: number + depreciation: number + interestExpense: number + taxes: number + netIncome: number + ebit: number + ihk: number + foundingCosts: number + // US GAAP aggregates + rAndD: number + sga: number +} + +function fmt(v: number): string { + if (Math.abs(v) >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` + if (Math.abs(v) >= 1_000) return `${(v / 1_000).toFixed(0)}k` + return Math.round(v).toLocaleString('de-DE') +} + +export default function AnnualPLTable({ results, lang, standard }: AnnualPLTableProps) { + const de = lang === 'de' + + // Aggregate monthly results into annual + const annualMap = new Map() + for (const r of results) { + if (!annualMap.has(r.year)) annualMap.set(r.year, []) + annualMap.get(r.year)!.push(r) + } + + const rows: AnnualRow[] = Array.from(annualMap.entries()).map(([year, months]) => { + const revenue = months.reduce((s, m) => s + m.revenue_eur, 0) + const cogs = months.reduce((s, m) => s + m.cogs_eur, 0) + const grossProfit = revenue - cogs + const personnel = months.reduce((s, m) => s + m.personnel_eur, 0) + const marketing = months.reduce((s, m) => s + m.marketing_eur, 0) + const infra = months.reduce((s, m) => s + m.infra_eur, 0) + const adminCosts = months.reduce((s, m) => s + (m.admin_costs_eur || 0), 0) + const officeCosts = months.reduce((s, m) => s + (m.office_costs_eur || 0), 0) + const travelCosts = months.reduce((s, m) => s + (m.travel_costs_eur || 0), 0) + const softwareLicenses = months.reduce((s, m) => s + (m.software_licenses_eur || 0), 0) + const depreciation = months.reduce((s, m) => s + (m.depreciation_eur || 0), 0) + const interestExpense = months.reduce((s, m) => s + (m.interest_expense_eur || 0), 0) + const taxes = months.reduce((s, m) => s + (m.taxes_eur || 0), 0) + const netIncome = months.reduce((s, m) => s + (m.net_income_eur || 0), 0) + const ebit = months.reduce((s, m) => s + (m.ebit_eur || 0), 0) + const ihk = months.reduce((s, m) => s + (m.ihk_eur || 0), 0) + const foundingCosts = months.reduce((s, m) => s + (m.founding_costs_eur || 0), 0) + + const totalOpex = personnel + marketing + infra + adminCosts + officeCosts + travelCosts + softwareLicenses + ihk + foundingCosts + const ebitda = grossProfit - totalOpex + const lastMonth = months[months.length - 1] + + // US GAAP aggregates + const rAndD = infra + softwareLicenses + const sga = personnel + adminCosts + officeCosts + travelCosts + ihk + foundingCosts + + return { + year, + revenue, + cogs, + grossProfit, + grossMarginPct: revenue > 0 ? (grossProfit / revenue) * 100 : 0, + personnel, + marketing, + infra, + totalOpex, + ebitda, + ebitdaMarginPct: revenue > 0 ? (ebitda / revenue) * 100 : 0, + customers: lastMonth.total_customers, + employees: lastMonth.employees_count, + adminCosts, + officeCosts, + travelCosts, + softwareLicenses, + depreciation, + interestExpense, + taxes, + netIncome, + ebit, + ihk, + foundingCosts, + rAndD, + sga, + } + }) + + type LineItem = { + label: string + getValue: (r: AnnualRow) => number + isBold?: boolean + isPercent?: boolean + isSeparator?: boolean + isNegative?: boolean + isSubRow?: boolean + } + + const hgbLineItems: LineItem[] = [ + { label: 'Umsatzerloese', getValue: r => r.revenue, isBold: true }, + { label: '- Materialaufwand', getValue: r => r.cogs, isNegative: true }, + { label: '- Personalaufwand', getValue: r => r.personnel, isNegative: true }, + { label: '- Abschreibungen', getValue: r => r.depreciation, isNegative: true }, + { label: '- Sonstige betr. Aufwendungen', getValue: r => r.marketing + r.adminCosts + r.officeCosts + r.travelCosts + r.softwareLicenses + r.ihk + r.foundingCosts + r.infra, isNegative: true, isSeparator: true }, + { label: ' davon Marketing', getValue: r => r.marketing, isNegative: true, isSubRow: true }, + { label: ' davon Steuerberater/Recht', getValue: r => r.adminCosts, isNegative: true, isSubRow: true }, + { label: ' davon Buero/Telefon', getValue: r => r.officeCosts, isNegative: true, isSubRow: true }, + { label: ' davon Software', getValue: r => r.softwareLicenses, isNegative: true, isSubRow: true }, + { label: ' davon Reisekosten', getValue: r => r.travelCosts, isNegative: true, isSubRow: true }, + { label: ' davon Infrastruktur', getValue: r => r.infra, isNegative: true, isSubRow: true }, + { label: '= Betriebsergebnis (EBIT)', getValue: r => r.ebit, isBold: true, isSeparator: true }, + { label: '- Zinsaufwand', getValue: r => r.interestExpense, isNegative: true }, + { label: '= Ergebnis vor Steuern (EBT)', getValue: r => r.ebit - r.interestExpense, isBold: true, isSeparator: true }, + { label: '- Steuern', getValue: r => r.taxes, isNegative: true }, + { label: '= Jahresueberschuss', getValue: r => r.netIncome, isBold: true, isSeparator: true }, + { label: 'Kunden (Jahresende)', getValue: r => r.customers }, + { label: 'Mitarbeiter', getValue: r => r.employees }, + ] + + const usgaapLineItems: LineItem[] = [ + { label: 'Revenue', getValue: r => r.revenue, isBold: true }, + { label: '- Cost of Revenue (COGS)', getValue: r => r.cogs, isNegative: true }, + { label: '= Gross Profit', getValue: r => r.grossProfit, isBold: true, isSeparator: true }, + { label: ' Gross Margin', getValue: r => r.grossMarginPct, isPercent: true }, + { label: '- Sales & Marketing', getValue: r => r.marketing, isNegative: true }, + { label: '- Research & Development', getValue: r => r.rAndD, isNegative: true }, + { label: '- General & Administrative', getValue: r => r.sga, isNegative: true }, + { label: '- Depreciation & Amortization', getValue: r => r.depreciation, isNegative: true }, + { label: '= Operating Income (EBIT)', getValue: r => r.ebit, isBold: true, isSeparator: true }, + { label: ' Operating Margin', getValue: r => r.revenue > 0 ? (r.ebit / r.revenue) * 100 : 0, isPercent: true }, + { label: '- Interest Expense', getValue: r => r.interestExpense, isNegative: true }, + { label: '= Income Before Tax (EBT)', getValue: r => r.ebit - r.interestExpense, isBold: true, isSeparator: true }, + { label: '- Income Tax', getValue: r => r.taxes, isNegative: true }, + { label: '= Net Income', getValue: r => r.netIncome, isBold: true, isSeparator: true }, + { label: 'Customers (Year End)', getValue: r => r.customers }, + { label: 'Employees', getValue: r => r.employees }, + ] + + const lineItems = standard === 'hgb' ? hgbLineItems : usgaapLineItems + + return ( + + + + + + {rows.map(r => ( + + ))} + + + + {lineItems.map((item, idx) => ( + + + {rows.map(r => { + const val = item.getValue(r) + return ( + + ) + })} + + ))} + +
+ {standard === 'hgb' + ? 'GuV-Position (HGB)' + : 'P&L Line Item (US GAAP)'} + + {r.year} +
+ {item.label} + 0 && (item.label.includes('EBIT') || item.label.includes('Net Income') || item.label.includes('Jahresueberschuss') || item.label.includes('Betriebsergebnis')) ? 'text-emerald-400' : ''} + ${!item.isPercent && !item.isBold && !item.isSubRow ? 'text-white/60' : 'text-white'} + `} + > + {item.isPercent + ? `${val.toFixed(1)}%` + : (item.isNegative && val > 0 ? '-' : '') + fmt(Math.abs(val)) + } +
+
+ ) +} diff --git a/pitch-deck/components/ui/BrandName.tsx b/pitch-deck/components/ui/BrandName.tsx new file mode 100644 index 0000000..edc3a91 --- /dev/null +++ b/pitch-deck/components/ui/BrandName.tsx @@ -0,0 +1,19 @@ +'use client' + +interface BrandNameProps { + className?: string + prefix?: boolean +} + +/** + * Renders "ComplAI" (or "BreakPilot ComplAI") with the "AI" portion + * styled as a gradient to visually distinguish lowercase-L from uppercase-I. + */ +export default function BrandName({ className = '', prefix = false }: BrandNameProps) { + return ( + + {prefix && <>BreakPilot } + ComplAI + + ) +} diff --git a/pitch-deck/components/ui/FeatureMatrix.tsx b/pitch-deck/components/ui/FeatureMatrix.tsx index 1e87cf8..b64c7d0 100644 --- a/pitch-deck/components/ui/FeatureMatrix.tsx +++ b/pitch-deck/components/ui/FeatureMatrix.tsx @@ -3,6 +3,7 @@ import { motion } from 'framer-motion' import { PitchFeature, Language } from '@/lib/types' import { Check, X, Star } from 'lucide-react' +import BrandName from './BrandName' interface FeatureMatrixProps { features: PitchFeature[] @@ -34,7 +35,7 @@ export default function FeatureMatrix({ features, lang }: FeatureMatrixProps) { Feature - ComplAI + Proliance DataGuard heyData diff --git a/pitch-deck/components/ui/FinancialChart.tsx b/pitch-deck/components/ui/FinancialChart.tsx index f843c6a..a53456b 100644 --- a/pitch-deck/components/ui/FinancialChart.tsx +++ b/pitch-deck/components/ui/FinancialChart.tsx @@ -1,10 +1,7 @@ 'use client' -import { PitchFinancial, Language } from '@/lib/types' -import { t } from '@/lib/i18n' +import { FMResult, FMComputeResponse } from '@/lib/types' import { - BarChart, - Bar, XAxis, YAxis, Tooltip, @@ -12,59 +9,190 @@ import { Line, ComposedChart, Area, + ReferenceLine, + Brush, } from 'recharts' interface FinancialChartProps { - financials: PitchFinancial[] - lang: Language - growthMultiplier?: number + activeResults: FMComputeResponse | null + compareResults?: Map + compareMode?: boolean + scenarioColors?: Record + lang: 'de' | 'en' } -export default function FinancialChart({ financials, lang, growthMultiplier = 1 }: FinancialChartProps) { - const i = t(lang) +export default function FinancialChart({ + activeResults, + compareResults, + compareMode = false, + scenarioColors = {}, + lang, +}: FinancialChartProps) { + if (!activeResults) { + return ( +
+ {lang === 'de' ? 'Lade Daten...' : 'Loading data...'} +
+ ) + } - const data = financials.map((f) => ({ - year: f.year, - [i.financials.revenue]: Math.round(f.revenue_eur * growthMultiplier), - [i.financials.costs]: f.costs_eur, - [i.financials.customers]: Math.round(f.customers_count * growthMultiplier), - })) + const results = activeResults.results + const breakEvenMonth = activeResults.summary.break_even_month + + // Build chart data — monthly + const data = results.map((r) => { + const entry: Record = { + label: `${r.year.toString().slice(2)}/${String(r.month_in_year).padStart(2, '0')}`, + month: r.month, + revenue: Math.round(r.revenue_eur), + costs: Math.round(r.total_costs_eur), + customers: r.total_customers, + cashBalance: Math.round(r.cash_balance_eur), + } + + // Add compare scenario data + if (compareMode && compareResults) { + compareResults.forEach((cr, scenarioId) => { + const crMonth = cr.results.find(m => m.month === r.month) + if (crMonth) { + entry[`revenue_${scenarioId}`] = Math.round(crMonth.revenue_eur) + entry[`customers_${scenarioId}`] = crMonth.total_customers + } + }) + } + + return entry + }) const formatValue = (value: number) => { - if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` - if (value >= 1_000) return `${(value / 1_000).toFixed(0)}k` + if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` + if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(0)}k` return value.toString() } return (
- + - - - + + + - - - + + + - - + + + + + { + const label = name === 'revenue' ? (lang === 'de' ? 'Umsatz' : 'Revenue') + : name === 'costs' ? (lang === 'de' ? 'Kosten' : 'Costs') + : name === 'customers' ? (lang === 'de' ? 'Kunden' : 'Customers') + : name === 'cashBalance' ? 'Cash' + : name + return [name === 'customers' ? value : formatValue(value) + ' EUR', label] }} - formatter={(value: number) => formatValue(value) + ' EUR'} /> - - - + + {/* Break-even reference line */} + {breakEvenMonth && ( + + )} + + {/* Revenue area */} + + + {/* Cost area */} + + + {/* Customers line */} + + + {/* Compare mode: overlay other scenarios */} + {compareMode && compareResults && Array.from(compareResults.entries()).map(([scenarioId]) => ( + + ))} + + {/* Brush for zooming */} +
diff --git a/pitch-deck/components/ui/FinancialSliders.tsx b/pitch-deck/components/ui/FinancialSliders.tsx index 5211da9..23ba225 100644 --- a/pitch-deck/components/ui/FinancialSliders.tsx +++ b/pitch-deck/components/ui/FinancialSliders.tsx @@ -1,52 +1,63 @@ 'use client' -import { Language } from '@/lib/types' -import { t } from '@/lib/i18n' +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { ChevronDown, ChevronRight } from 'lucide-react' +import { FMAssumption, Language } from '@/lib/types' interface FinancialSlidersProps { - growthRate: number - churnRate: number - arpu: number - onGrowthChange: (v: number) => void - onChurnChange: (v: number) => void - onArpuChange: (v: number) => void + assumptions: FMAssumption[] + onAssumptionChange: (key: string, value: number) => void lang: Language } function Slider({ - label, - value, - min, - max, - step, - unit, + assumption, onChange, + lang, }: { - label: string - value: number - min: number - max: number - step: number - unit: string - onChange: (v: number) => void + assumption: FMAssumption + onChange: (value: number) => void + lang: Language }) { + const value = typeof assumption.value === 'number' ? assumption.value : Number(assumption.value) + const label = lang === 'de' ? assumption.label_de : assumption.label_en + + if (assumption.value_type === 'step') { + // Display step values as read-only list + const steps = Array.isArray(assumption.value) ? assumption.value : [] + return ( +
+

{label}

+
+ {steps.map((s: number, i: number) => ( +
+

Y{i + 1}

+

{s}

+
+ ))} +
+
+ ) + } + return ( -
-
- {label} - {value}{unit} +
+
+ {label} + {value}{assumption.unit === 'EUR' ? ' EUR' : assumption.unit === '%' ? '%' : ` ${assumption.unit || ''}`}
onChange(parseFloat(e.target.value))} className="w-full h-1.5 bg-white/10 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none - [&::-webkit-slider-thumb]:w-4 - [&::-webkit-slider-thumb]:h-4 + [&::-webkit-slider-thumb]:w-3.5 + [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-indigo-500 [&::-webkit-slider-thumb]:shadow-lg @@ -58,47 +69,76 @@ function Slider({ ) } -export default function FinancialSliders({ - growthRate, - churnRate, - arpu, - onGrowthChange, - onChurnChange, - onArpuChange, - lang, -}: FinancialSlidersProps) { - const i = t(lang) +interface CategoryGroup { + key: string + label: string + items: FMAssumption[] +} + +export default function FinancialSliders({ assumptions, onAssumptionChange, lang }: FinancialSlidersProps) { + const [openCategories, setOpenCategories] = useState>(new Set(['revenue'])) + + // Group assumptions by category + const categories: CategoryGroup[] = [ + { key: 'revenue', label: lang === 'de' ? 'Revenue' : 'Revenue', items: [] }, + { key: 'costs', label: lang === 'de' ? 'Kosten' : 'Costs', items: [] }, + { key: 'team', label: 'Team', items: [] }, + { key: 'admin', label: lang === 'de' ? 'Verwaltung' : 'Administration', items: [] }, + { key: 'funding', label: 'Funding', items: [] }, + ] + + for (const a of assumptions) { + const cat = categories.find(c => c.key === a.category) || categories[0] + cat.items.push(a) + } + + const toggleCategory = (key: string) => { + setOpenCategories(prev => { + const next = new Set(prev) + if (next.has(key)) next.delete(key) + else next.add(key) + return next + }) + } return ( -
-

{i.financials.adjustAssumptions}

- - - +
+ {categories.filter(c => c.items.length > 0).map((cat) => { + const isOpen = openCategories.has(cat.key) + return ( +
+ + + {isOpen && ( + +
+ {cat.items.map((a) => ( + onAssumptionChange(a.key, val)} + lang={lang} + /> + ))} +
+
+ )} +
+
+ ) + })}
) } diff --git a/pitch-deck/components/ui/KPICard.tsx b/pitch-deck/components/ui/KPICard.tsx new file mode 100644 index 0000000..28f7768 --- /dev/null +++ b/pitch-deck/components/ui/KPICard.tsx @@ -0,0 +1,59 @@ +'use client' + +import { motion } from 'framer-motion' +import { TrendingUp, TrendingDown } from 'lucide-react' +import AnimatedCounter from './AnimatedCounter' + +interface KPICardProps { + label: string + value: number + prefix?: string + suffix?: string + decimals?: number + trend?: 'up' | 'down' | 'neutral' + color?: string + delay?: number + subLabel?: string +} + +export default function KPICard({ + label, + value, + prefix = '', + suffix = '', + decimals = 0, + trend = 'neutral', + color = '#6366f1', + delay = 0, + subLabel, +}: KPICardProps) { + return ( + + {/* Glow effect */} +
+ +

{label}

+
+

+ +

+ {trend !== 'neutral' && ( + + {trend === 'up' ? : } + + )} +
+ {subLabel && ( +

{subLabel}

+ )} + + ) +} diff --git a/pitch-deck/components/ui/RunwayGauge.tsx b/pitch-deck/components/ui/RunwayGauge.tsx new file mode 100644 index 0000000..19f186f --- /dev/null +++ b/pitch-deck/components/ui/RunwayGauge.tsx @@ -0,0 +1,133 @@ +'use client' + +import { motion } from 'framer-motion' +import { useEffect, useState } from 'react' + +interface RunwayGaugeProps { + months: number + maxMonths?: number + size?: number + label?: string +} + +export default function RunwayGauge({ months, maxMonths = 36, size = 140, label = 'Runway' }: RunwayGaugeProps) { + const [animatedAngle, setAnimatedAngle] = useState(0) + const clampedMonths = Math.min(months, maxMonths) + const targetAngle = (clampedMonths / maxMonths) * 270 - 135 // -135 to +135 degrees + + useEffect(() => { + const timer = setTimeout(() => setAnimatedAngle(targetAngle), 100) + return () => clearTimeout(timer) + }, [targetAngle]) + + // Color based on runway + const getColor = () => { + if (months >= 18) return '#22c55e' // green + if (months >= 12) return '#eab308' // yellow + if (months >= 6) return '#f97316' // orange + return '#ef4444' // red + } + + const color = getColor() + const cx = size / 2 + const cy = size / 2 + const radius = (size / 2) - 16 + const needleLength = radius - 10 + + // Arc path for gauge background + const startAngle = -135 + const endAngle = 135 + const polarToCartesian = (cx: number, cy: number, r: number, deg: number) => { + const rad = (deg - 90) * Math.PI / 180 + return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) } + } + + const arcStart = polarToCartesian(cx, cy, radius, startAngle) + const arcEnd = polarToCartesian(cx, cy, radius, endAngle) + const arcPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 1 1 ${arcEnd.x} ${arcEnd.y}` + + // Filled arc + const filledEnd = polarToCartesian(cx, cy, radius, Math.min(animatedAngle, endAngle)) + const largeArc = (animatedAngle - startAngle) > 180 ? 1 : 0 + const filledPath = `M ${arcStart.x} ${arcStart.y} A ${radius} ${radius} 0 ${largeArc} 1 ${filledEnd.x} ${filledEnd.y}` + + // Needle endpoint + const needleRad = (animatedAngle - 90) * Math.PI / 180 + const needleX = cx + needleLength * Math.cos(needleRad) + const needleY = cy + needleLength * Math.sin(needleRad) + + const shouldPulse = months < 6 + + return ( + +
+ + {/* Background arc */} + + + {/* Filled arc */} + + + {/* Tick marks */} + {[0, 6, 12, 18, 24, 30, 36].map((tick) => { + const tickAngle = (tick / maxMonths) * 270 - 135 + const inner = polarToCartesian(cx, cy, radius - 12, tickAngle) + const outer = polarToCartesian(cx, cy, radius - 6, tickAngle) + return ( + + + + {tick} + + + ) + })} + + {/* Needle */} + + + {/* Center circle */} + + + +
+ +
+

{Math.round(months)}

+

{label}

+
+
+ ) +} diff --git a/pitch-deck/components/ui/ScenarioSwitcher.tsx b/pitch-deck/components/ui/ScenarioSwitcher.tsx new file mode 100644 index 0000000..07d4991 --- /dev/null +++ b/pitch-deck/components/ui/ScenarioSwitcher.tsx @@ -0,0 +1,63 @@ +'use client' + +import { FMScenario } from '@/lib/types' +import { motion } from 'framer-motion' + +interface ScenarioSwitcherProps { + scenarios: FMScenario[] + activeId: string | null + compareMode: boolean + onSelect: (id: string) => void + onToggleCompare: () => void + lang: 'de' | 'en' +} + +export default function ScenarioSwitcher({ + scenarios, + activeId, + compareMode, + onSelect, + onToggleCompare, + lang, +}: ScenarioSwitcherProps) { + return ( +
+
+

+ {lang === 'de' ? 'Szenarien' : 'Scenarios'} +

+ +
+ +
+ {scenarios.map((s) => ( + onSelect(s.id)} + className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-all + ${activeId === s.id + ? 'bg-white/[0.12] border border-white/20 text-white' + : 'bg-white/[0.04] border border-white/10 text-white/50 hover:text-white/70' + }`} + > + + {s.name} + + ))} +
+
+ ) +} diff --git a/pitch-deck/components/ui/UnitEconomicsCards.tsx b/pitch-deck/components/ui/UnitEconomicsCards.tsx new file mode 100644 index 0000000..9fb336a --- /dev/null +++ b/pitch-deck/components/ui/UnitEconomicsCards.tsx @@ -0,0 +1,93 @@ +'use client' + +import { motion } from 'framer-motion' +import AnimatedCounter from './AnimatedCounter' + +interface UnitEconomicsCardsProps { + cac: number + ltv: number + ltvCacRatio: number + grossMargin: number + churnRate: number + lang: 'de' | 'en' +} + +function MiniRing({ progress, color, size = 32 }: { progress: number; color: string; size?: number }) { + const radius = (size / 2) - 3 + const circumference = 2 * Math.PI * radius + const offset = circumference - (Math.min(progress, 100) / 100) * circumference + + return ( + + + + + ) +} + +export default function UnitEconomicsCards({ cac, ltv, ltvCacRatio, grossMargin, churnRate, lang }: UnitEconomicsCardsProps) { + const cacPayback = cac > 0 ? Math.ceil(cac / ((ltv / (1 / (churnRate / 100))) || 1)) : 0 + + const cards = [ + { + label: 'CAC Payback', + value: cacPayback, + suffix: lang === 'de' ? ' Mo.' : ' mo.', + ring: Math.min((cacPayback / 12) * 100, 100), + color: cacPayback <= 6 ? '#22c55e' : cacPayback <= 12 ? '#eab308' : '#ef4444', + sub: `CAC: ${cac.toLocaleString('de-DE')} EUR`, + }, + { + label: 'LTV', + value: Math.round(ltv), + suffix: ' EUR', + ring: Math.min(ltvCacRatio * 10, 100), + color: ltvCacRatio >= 3 ? '#22c55e' : ltvCacRatio >= 1.5 ? '#eab308' : '#ef4444', + sub: `LTV/CAC: ${ltvCacRatio.toFixed(1)}x`, + }, + { + label: 'Gross Margin', + value: grossMargin, + suffix: '%', + ring: grossMargin, + color: grossMargin >= 70 ? '#22c55e' : grossMargin >= 50 ? '#eab308' : '#ef4444', + sub: `Churn: ${churnRate}%`, + }, + ] + + return ( +
+ {cards.map((card, i) => ( + +
+ +
+

+ +

+

{card.label}

+

{card.sub}

+
+ ))} +
+ ) +} diff --git a/pitch-deck/components/ui/WaterfallChart.tsx b/pitch-deck/components/ui/WaterfallChart.tsx new file mode 100644 index 0000000..3cb35ae --- /dev/null +++ b/pitch-deck/components/ui/WaterfallChart.tsx @@ -0,0 +1,85 @@ +'use client' + +import { FMResult } from '@/lib/types' +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + ReferenceLine, + Cell, +} from 'recharts' + +interface WaterfallChartProps { + results: FMResult[] + lang: 'de' | 'en' +} + +export default function WaterfallChart({ results, lang }: WaterfallChartProps) { + // Sample quarterly data for cleaner display + const quarterlyData = results.filter((_, i) => i % 3 === 0).map((r) => { + const netCash = r.revenue_eur - r.total_costs_eur + return { + label: `${r.year.toString().slice(2)}/Q${Math.ceil(r.month_in_year / 3)}`, + month: r.month, + revenue: Math.round(r.revenue_eur), + costs: Math.round(-r.total_costs_eur), + net: Math.round(netCash), + cashBalance: Math.round(r.cash_balance_eur), + } + }) + + const formatValue = (value: number) => { + if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M` + if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(0)}k` + return value.toString() + } + + return ( +
+ + + + + [ + formatValue(value) + ' EUR', + name === 'revenue' ? (lang === 'de' ? 'Umsatz' : 'Revenue') + : name === 'costs' ? (lang === 'de' ? 'Kosten' : 'Costs') + : 'Net', + ]} + /> + + + {quarterlyData.map((entry, i) => ( + + ))} + + + {quarterlyData.map((entry, i) => ( + + ))} + + + +
+ ) +} diff --git a/pitch-deck/lib/hooks/useFinancialModel.ts b/pitch-deck/lib/hooks/useFinancialModel.ts new file mode 100644 index 0000000..051e00e --- /dev/null +++ b/pitch-deck/lib/hooks/useFinancialModel.ts @@ -0,0 +1,109 @@ +'use client' + +import { useState, useEffect, useCallback, useRef } from 'react' +import { FMScenario, FMResult, FMComputeResponse } from '../types' + +export function useFinancialModel() { + const [scenarios, setScenarios] = useState([]) + const [activeScenarioId, setActiveScenarioId] = useState(null) + const [compareMode, setCompareMode] = useState(false) + const [results, setResults] = useState>(new Map()) + const [loading, setLoading] = useState(true) + const [computing, setComputing] = useState(false) + const computeTimer = useRef(null) + + // Load scenarios on mount + useEffect(() => { + async function load() { + try { + const res = await fetch('/api/financial-model') + if (res.ok) { + const data: FMScenario[] = await res.json() + setScenarios(data) + const defaultScenario = data.find(s => s.is_default) || data[0] + if (defaultScenario) { + setActiveScenarioId(defaultScenario.id) + } + } + } catch (err) { + console.error('Failed to load financial model:', err) + } finally { + setLoading(false) + } + } + load() + }, []) + + // Compute when active scenario changes + useEffect(() => { + if (activeScenarioId && !results.has(activeScenarioId)) { + compute(activeScenarioId) + } + }, [activeScenarioId]) // eslint-disable-line react-hooks/exhaustive-deps + + const compute = useCallback(async (scenarioId: string) => { + setComputing(true) + try { + const res = await fetch('/api/financial-model/compute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scenarioId }), + }) + if (res.ok) { + const data: FMComputeResponse = await res.json() + setResults(prev => new Map(prev).set(scenarioId, data)) + } + } catch (err) { + console.error('Compute failed:', err) + } finally { + setComputing(false) + } + }, []) + + const updateAssumption = useCallback(async (scenarioId: string, key: string, value: number | number[]) => { + // Optimistic update in local state + setScenarios(prev => prev.map(s => { + if (s.id !== scenarioId) return s + return { + ...s, + assumptions: s.assumptions.map(a => a.key === key ? { ...a, value } : a), + } + })) + + // Save to DB + await fetch('/api/financial-model/assumptions', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scenarioId, key, value }), + }) + + // Debounced recompute + if (computeTimer.current) clearTimeout(computeTimer.current) + computeTimer.current = setTimeout(() => compute(scenarioId), 300) + }, [compute]) + + const computeAll = useCallback(async () => { + for (const s of scenarios) { + await compute(s.id) + } + }, [scenarios, compute]) + + const activeScenario = scenarios.find(s => s.id === activeScenarioId) || null + const activeResults = activeScenarioId ? results.get(activeScenarioId) || null : null + + return { + scenarios, + activeScenario, + activeScenarioId, + setActiveScenarioId, + activeResults, + results, + loading, + computing, + compareMode, + setCompareMode, + compute, + computeAll, + updateAssumption, + } +} diff --git a/pitch-deck/lib/hooks/useSlideNavigation.ts b/pitch-deck/lib/hooks/useSlideNavigation.ts index fb26863..a4d6fb2 100644 --- a/pitch-deck/lib/hooks/useSlideNavigation.ts +++ b/pitch-deck/lib/hooks/useSlideNavigation.ts @@ -14,9 +14,24 @@ const SLIDE_ORDER: SlideId[] = [ 'traction', 'competition', 'team', + 'technology', 'financials', 'the-ask', 'ai-qa', + 'appendix', + 'annex-infra', + 'annex-ai-stack', + 'annex-rag', + 'annex-security', + 'annex-devops', + 'annex-agent-arch', + 'annex-agent-rag', + 'annex-agent-workflow', + 'annex-usp-overview', + 'annex-usp-comparison', + 'annex-usp-moat', + 'annex-roadmap-2027', + 'annex-roadmap-2028', ] export const TOTAL_SLIDES = SLIDE_ORDER.length diff --git a/pitch-deck/lib/i18n.ts b/pitch-deck/lib/i18n.ts index 64a8dec..24f849a 100644 --- a/pitch-deck/lib/i18n.ts +++ b/pitch-deck/lib/i18n.ts @@ -18,13 +18,28 @@ const translations = { 'Traction', 'Wettbewerb', 'Team', + 'Technologie', 'Finanzen', 'The Ask', 'KI Q&A', + 'Appendix', + 'Infrastruktur', + 'KI-Stack', + 'RAG Pipeline', + 'Sicherheit', + 'DevOps & CI/CD', + 'Agent Architektur', + 'Rechtsdokumente', + 'Compliance Workflow', + '5 USPs', + 'Wettbewerbsvergleich', + 'Marktposition', + 'Roadmap 2027', + 'Roadmap 2028', ], cover: { tagline: 'Datensouveraenitaet meets KI-Compliance', - subtitle: 'Pre-Seed · Q4 2026', + subtitle: 'Seed · 2026-2027', cta: 'Pitch starten', }, problem: { @@ -148,6 +163,46 @@ const translations = { equity: 'Equity', expertise: 'Expertise', }, + technology: { + title: 'Technologie-Roadmap', + subtitle: 'Von MVP zu Full Autonomy — 5-Jahres-Technologieplan', + timelineTitle: 'Meilensteine 2026-2030', + stackTitle: 'Tech-Stack', + phases: [ + { + year: '2026', + phase: 'Foundation & MVP', + techs: ['Self-Hosted LLM (32B)', 'Apple M2 Mini', 'Basic Compliance SDK', 'OCR Pipeline'], + }, + { + year: '2027', + phase: 'Product-Market Fit', + techs: ['Multi-Model Router', 'RAG 2.0 mit Qdrant', 'Auto Compliance-Scan', 'Echtzeit-Monitoring'], + }, + { + year: '2028', + phase: 'Enterprise Scale', + techs: ['Federated Learning', 'Cluster-Management', 'API Marketplace', 'Multi-Tenant'], + }, + { + year: '2029', + phase: 'AI Platform', + techs: ['Custom Fine-Tuned (40B+)', 'Predictive Compliance', 'Auto-Audits', 'Partner-Integrationen'], + }, + { + year: '2030', + phase: 'Full Autonomy', + techs: ['Agent-Netzwerk', 'Self-Healing', 'Zero-Day Compliance', '1000B Parameter'], + }, + ], + layers: [ + { name: 'Application', techs: 'Next.js, React, TailwindCSS, REST/GraphQL API' }, + { name: 'AI/ML', techs: 'Qwen 32B → 1000B, RAG Pipeline, NLP, OCR (PaddleOCR)' }, + { name: 'Infrastructure', techs: 'Apple Silicon (M2/M4 Pro), Docker, Self-Hosted' }, + { name: 'Security', techs: 'BSI-TR-03161, E2E Verschluesselung, Vault' }, + { name: 'Data', techs: 'PostgreSQL + PostGIS, Qdrant (Vektoren), MinIO (S3)' }, + ], + }, financials: { title: 'Finanzprognose', subtitle: 'AI-First Kostenstruktur — skaliert ohne lineares Personalwachstum', @@ -166,16 +221,17 @@ const translations = { }, theAsk: { title: 'The Ask', - subtitle: 'Pre-Seed Finanzierung', + subtitle: 'Gestaffelte Finanzierung 2026-2027', amount: 'Funding', instrument: 'Instrument', useOfFunds: 'Use of Funds', engineering: 'Engineering', sales: 'Vertrieb', hardware: 'Hardware', + personnel: 'Personal', legal: 'Legal', reserve: 'Reserve', - targetDate: 'Zieldatum', + targetDate: 'Zeitraum', }, aiqa: { title: 'Fragen? Die KI antwortet.', @@ -208,13 +264,28 @@ const translations = { 'Traction', 'Competition', 'Team', + 'Technology', 'Financials', 'The Ask', 'AI Q&A', + 'Appendix', + 'Infrastructure', + 'AI Stack', + 'RAG Pipeline', + 'Security', + 'DevOps & CI/CD', + 'Agent Architecture', + 'Legal Documents', + 'Compliance Workflow', + '5 USPs', + 'Competitor Comparison', + 'Market Position', + 'Roadmap 2027', + 'Roadmap 2028', ], cover: { tagline: 'Data Sovereignty meets AI Compliance', - subtitle: 'Pre-Seed · Q4 2026', + subtitle: 'Seed · 2026-2027', cta: 'Start Pitch', }, problem: { @@ -338,6 +409,46 @@ const translations = { equity: 'Equity', expertise: 'Expertise', }, + technology: { + title: 'Technology Roadmap', + subtitle: 'From MVP to Full Autonomy — 5-Year Technology Plan', + timelineTitle: 'Milestones 2026-2030', + stackTitle: 'Tech Stack', + phases: [ + { + year: '2026', + phase: 'Foundation & MVP', + techs: ['Self-Hosted LLM (32B)', 'Apple M2 Mini', 'Basic Compliance SDK', 'OCR Pipeline'], + }, + { + year: '2027', + phase: 'Product-Market Fit', + techs: ['Multi-Model Router', 'RAG 2.0 with Qdrant', 'Auto Compliance Scan', 'Real-time Monitoring'], + }, + { + year: '2028', + phase: 'Enterprise Scale', + techs: ['Federated Learning', 'Cluster Management', 'API Marketplace', 'Multi-Tenant'], + }, + { + year: '2029', + phase: 'AI Platform', + techs: ['Custom Fine-Tuned (40B+)', 'Predictive Compliance', 'Auto-Audits', 'Partner Integrations'], + }, + { + year: '2030', + phase: 'Full Autonomy', + techs: ['Agent Network', 'Self-Healing', 'Zero-Day Compliance', '1000B Parameters'], + }, + ], + layers: [ + { name: 'Application', techs: 'Next.js, React, TailwindCSS, REST/GraphQL API' }, + { name: 'AI/ML', techs: 'Qwen 32B → 1000B, RAG Pipeline, NLP, OCR (PaddleOCR)' }, + { name: 'Infrastructure', techs: 'Apple Silicon (M2/M4 Pro), Docker, Self-Hosted' }, + { name: 'Security', techs: 'BSI-TR-03161, E2E Encryption, Vault' }, + { name: 'Data', techs: 'PostgreSQL + PostGIS, Qdrant (Vectors), MinIO (S3)' }, + ], + }, financials: { title: 'Financial Projections', subtitle: 'AI-First cost structure — scales without linear headcount growth', @@ -356,16 +467,17 @@ const translations = { }, theAsk: { title: 'The Ask', - subtitle: 'Pre-Seed Funding', + subtitle: 'Staged Funding 2026-2027', amount: 'Funding', instrument: 'Instrument', useOfFunds: 'Use of Funds', engineering: 'Engineering', sales: 'Sales', hardware: 'Hardware', + personnel: 'Personnel', legal: 'Legal', reserve: 'Reserve', - targetDate: 'Target Date', + targetDate: 'Timeline', }, aiqa: { title: 'Questions? The AI answers.', diff --git a/pitch-deck/lib/types.ts b/pitch-deck/lib/types.ts index 045298a..31cf342 100644 --- a/pitch-deck/lib/types.ts +++ b/pitch-deck/lib/types.ts @@ -127,6 +127,85 @@ export interface PitchData { products: PitchProduct[] } +// Financial Model Types +export interface FMScenario { + id: string + name: string + description: string + is_default: boolean + color: string + assumptions: FMAssumption[] +} + +export interface FMAssumption { + id: string + scenario_id: string + key: string + label_de: string + label_en: string + value: number | number[] + value_type: 'scalar' | 'step' | 'timeseries' + unit: string + min_value: number | null + max_value: number | null + step_size: number | null + category: string + sort_order: number +} + +export interface FMResult { + month: number + year: number + month_in_year: number + new_customers: number + churned_customers: number + total_customers: number + mrr_eur: number + arr_eur: number + revenue_eur: number + cogs_eur: number + personnel_eur: number + infra_eur: number + marketing_eur: number + total_costs_eur: number + employees_count: number + gross_margin_pct: number + burn_rate_eur: number + runway_months: number + cac_eur: number + ltv_eur: number + ltv_cac_ratio: number + cash_balance_eur: number + cumulative_revenue_eur: number + // Detail costs + admin_costs_eur: number + office_costs_eur: number + founding_costs_eur: number + ihk_eur: number + depreciation_eur: number + interest_expense_eur: number + taxes_eur: number + net_income_eur: number + ebit_eur: number + software_licenses_eur: number + travel_costs_eur: number + funding_eur: number +} + +export interface FMComputeResponse { + scenario_id: string + results: FMResult[] + summary: { + final_arr: number + final_customers: number + break_even_month: number | null + final_runway: number + final_ltv_cac: number + peak_burn: number + total_funding_needed: number + } +} + export type Language = 'de' | 'en' export interface ChatMessage { @@ -145,6 +224,21 @@ export type SlideId = | 'traction' | 'competition' | 'team' + | 'technology' | 'financials' | 'the-ask' | 'ai-qa' + | 'appendix' + | 'annex-infra' + | 'annex-ai-stack' + | 'annex-rag' + | 'annex-security' + | 'annex-devops' + | 'annex-agent-arch' + | 'annex-agent-rag' + | 'annex-agent-workflow' + | 'annex-usp-overview' + | 'annex-usp-comparison' + | 'annex-usp-moat' + | 'annex-roadmap-2027' + | 'annex-roadmap-2028'