feat: Add staged funding model, financial compute engine, annex slides and UI enhancements
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string> {
|
||||
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<string, { de: string; en: string; index: number }> = {
|
||||
'cover': { de: 'Cover', en: 'Cover', index: 0 },
|
||||
'problem': { de: 'Das Problem', en: 'The Problem', index: 1 },
|
||||
'solution': { de: 'Die Loesung', en: 'The Solution', index: 2 },
|
||||
'product': { de: 'Produkte', en: 'Products', index: 3 },
|
||||
'how-it-works': { de: 'So funktionierts', en: 'How It Works', index: 4 },
|
||||
'market': { de: 'Markt', en: 'Market', index: 5 },
|
||||
'business-model': { de: 'Geschaeftsmodell', en: 'Business Model', index: 6 },
|
||||
'traction': { de: 'Traction', en: 'Traction', index: 7 },
|
||||
'competition': { de: 'Wettbewerb', en: 'Competition', index: 8 },
|
||||
'team': { de: 'Team', en: 'Team', index: 9 },
|
||||
'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`, {
|
||||
|
||||
30
pitch-deck/app/api/financial-model/assumptions/route.ts
Normal file
30
pitch-deck/app/api/financial-model/assumptions/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
388
pitch-deck/app/api/financial-model/compute/route.ts
Normal file
388
pitch-deck/app/api/financial-model/compute/route.ts
Normal file
@@ -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<string, number | number[] | FundingEvent[] | SalaryStep[]> = {}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
73
pitch-deck/app/api/financial-model/route.ts
Normal file
73
pitch-deck/app/api/financial-model/route.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
420
pitch-deck/components/ChatFAB.tsx
Normal file
420
pitch-deck/components/ChatFAB.tsx
Normal file
@@ -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<number>
|
||||
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<ChatMessage[]>([])
|
||||
const [input, setInput] = useState('')
|
||||
const [isStreaming, setIsStreaming] = useState(false)
|
||||
const [parsedResponses, setParsedResponses] = useState<Map<number, ParsedMessage>>(new Map())
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const abortRef = useRef<AbortController | null>(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 (
|
||||
<>
|
||||
<div className="whitespace-pre-wrap">{displayText}</div>
|
||||
{isStreaming && idx === messages.length - 1 && msg.role === 'assistant' && (
|
||||
<span className="inline-block w-1.5 h-3.5 bg-indigo-400 animate-pulse ml-0.5" />
|
||||
)}
|
||||
|
||||
{/* GOTO Buttons */}
|
||||
{parsed && parsed.gotos.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{parsed.gotos.map((g, gi) => (
|
||||
<button
|
||||
key={gi}
|
||||
onClick={() => onGoToSlide(g.index)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg
|
||||
bg-indigo-500/20 border border-indigo-500/30
|
||||
hover:bg-indigo-500/30 transition-colors
|
||||
text-xs text-indigo-300"
|
||||
>
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
{g.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Follow-Up Suggestions */}
|
||||
{parsed && parsed.followUps.length > 0 && !isStreaming && (
|
||||
<div className="mt-3 space-y-1.5 border-t border-white/10 pt-2">
|
||||
{parsed.followUps.map((q, qi) => (
|
||||
<button
|
||||
key={qi}
|
||||
onClick={() => sendMessage(q)}
|
||||
className="block w-full text-left px-2.5 py-2 rounded-lg
|
||||
bg-white/[0.05] border border-white/10
|
||||
hover:bg-white/[0.1] transition-colors
|
||||
text-xs text-white/60 hover:text-white/90"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* FAB Button — sits to the left of NavigationFAB */}
|
||||
<AnimatePresence>
|
||||
{!isOpen && (
|
||||
<motion.button
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0 }}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => 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'}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
||||
<circle cx="9" cy="10" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="10" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="10" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Chat Panel */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`fixed bottom-6 right-6 z-50
|
||||
${isExpanded ? 'w-[700px] h-[80vh]' : 'w-[400px] h-[520px]'}
|
||||
rounded-2xl overflow-hidden
|
||||
bg-black/90 backdrop-blur-xl border border-white/10
|
||||
shadow-2xl shadow-black/50 flex flex-col
|
||||
transition-all duration-200`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-indigo-500/20 flex items-center justify-center">
|
||||
<Bot className="w-4 h-4 text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-semibold text-white">Investor Agent</span>
|
||||
<span className="text-xs text-white/30 ml-2">
|
||||
{isStreaming
|
||||
? (lang === 'de' ? 'antwortet...' : 'responding...')
|
||||
: (lang === 'de' ? 'online' : 'online')
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(prev => !prev)}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
|
||||
>
|
||||
{isExpanded ? <Minimize2 className="w-3.5 h-3.5 text-white/60" /> : <Maximize2 className="w-3.5 h-3.5 text-white/60" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-white/60" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-3">
|
||||
{messages.length === 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-white/40 text-xs mb-3">
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>{lang === 'de' ? 'Fragen Sie den Investor Agent:' : 'Ask the Investor Agent:'}</span>
|
||||
</div>
|
||||
{suggestions.map((q, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 + idx * 0.08 }}
|
||||
onClick={() => 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}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex gap-2.5 ${msg.role === 'user' ? 'justify-end' : ''}`}
|
||||
>
|
||||
{msg.role === 'assistant' && (
|
||||
<div className="w-7 h-7 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Bot className="w-3.5 h-3.5 text-indigo-400" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`
|
||||
max-w-[85%] rounded-2xl px-3.5 py-2.5 text-xs leading-relaxed
|
||||
${msg.role === 'user'
|
||||
? 'bg-indigo-500/20 text-white'
|
||||
: 'bg-white/[0.06] text-white/80'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{msg.role === 'assistant' ? renderMessageContent(msg, idx) : (
|
||||
<div className="whitespace-pre-wrap">{msg.content}</div>
|
||||
)}
|
||||
</div>
|
||||
{msg.role === 'user' && (
|
||||
<div className="w-7 h-7 rounded-full bg-white/10 flex items-center justify-center shrink-0 mt-0.5">
|
||||
<User className="w-3.5 h-3.5 text-white/60" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="border-t border-white/10 px-4 py-3 shrink-0">
|
||||
{isStreaming && (
|
||||
<button
|
||||
onClick={stopGeneration}
|
||||
className="w-full mb-2 px-3 py-1.5 rounded-lg bg-white/[0.06] hover:bg-white/[0.1]
|
||||
text-xs text-white/50 transition-colors"
|
||||
>
|
||||
{lang === 'de' ? 'Antwort stoppen' : 'Stop response'}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => sendMessage()}
|
||||
disabled={isStreaming || !input.trim()}
|
||||
className="px-3.5 py-2.5 bg-indigo-500 hover:bg-indigo-600 disabled:opacity-30
|
||||
rounded-xl transition-all text-white"
|
||||
>
|
||||
<Send className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onGoToSlide(idx)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-2.5 text-left
|
||||
transition-all text-sm
|
||||
${isActive
|
||||
? 'bg-indigo-500/20 border-l-2 border-indigo-500 text-white'
|
||||
: 'hover:bg-white/[0.06] text-white/60 hover:text-white border-l-2 border-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={`
|
||||
w-6 h-6 rounded-full flex items-center justify-center text-xs font-mono shrink-0
|
||||
${isActive
|
||||
? 'bg-indigo-500 text-white'
|
||||
: isVisited
|
||||
? 'bg-white/10 text-white/60'
|
||||
: 'bg-white/5 text-white/30'
|
||||
}
|
||||
`}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{name}</span>
|
||||
{isAI && <Bot className="w-4 h-4 text-indigo-400 shrink-0" />}
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-indigo-400 shrink-0" />
|
||||
<div key={idx}>
|
||||
{idx === 14 && (
|
||||
<div className="mx-4 my-1 border-t border-white/10 pt-1">
|
||||
<span className="text-[9px] text-white/20 uppercase tracking-wider">Appendix</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onGoToSlide(idx)}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-4 py-2.5 text-left
|
||||
transition-all text-sm
|
||||
${isActive
|
||||
? 'bg-indigo-500/20 border-l-2 border-indigo-500 text-white'
|
||||
: 'hover:bg-white/[0.06] text-white/60 hover:text-white border-l-2 border-transparent'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={`
|
||||
w-6 h-6 rounded-full flex items-center justify-center text-xs font-mono shrink-0
|
||||
${isActive
|
||||
? 'bg-indigo-500 text-white'
|
||||
: isVisited
|
||||
? 'bg-white/10 text-white/60'
|
||||
: 'bg-white/5 text-white/30'
|
||||
}
|
||||
`}>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{name}</span>
|
||||
{isAI && <Bot className="w-4 h-4 text-indigo-400 shrink-0" />}
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-indigo-400 shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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 <CompetitionSlide lang={lang} features={data.features} competitors={data.competitors} />
|
||||
case 'team':
|
||||
return <TeamSlide lang={lang} team={data.team} />
|
||||
case 'technology':
|
||||
return <TechnologySlide lang={lang} />
|
||||
case 'financials':
|
||||
return <FinancialsSlide lang={lang} financials={data.financials} />
|
||||
return <FinancialsSlide lang={lang} />
|
||||
case 'the-ask':
|
||||
return <TheAskSlide lang={lang} funding={data.funding} />
|
||||
case 'ai-qa':
|
||||
return <AIQASlide lang={lang} />
|
||||
case 'appendix':
|
||||
return <AppendixSlide lang={lang} />
|
||||
case 'annex-infra':
|
||||
return <AnnexInfraSlide lang={lang} />
|
||||
case 'annex-ai-stack':
|
||||
return <AnnexAIStackSlide lang={lang} />
|
||||
case 'annex-rag':
|
||||
return <AnnexRAGSlide lang={lang} />
|
||||
case 'annex-security':
|
||||
return <AnnexSecuritySlide lang={lang} />
|
||||
case 'annex-devops':
|
||||
return <AnnexDevOpsSlide lang={lang} />
|
||||
case 'annex-agent-arch':
|
||||
return <AnnexAgentArchSlide lang={lang} />
|
||||
case 'annex-agent-rag':
|
||||
return <AnnexAgentRAGSlide lang={lang} />
|
||||
case 'annex-agent-workflow':
|
||||
return <AnnexAgentWorkflowSlide lang={lang} />
|
||||
case 'annex-usp-overview':
|
||||
return <AnnexUSPOverviewSlide lang={lang} />
|
||||
case 'annex-usp-comparison':
|
||||
return <AnnexUSPComparisonSlide lang={lang} />
|
||||
case 'annex-usp-moat':
|
||||
return <AnnexUSPMoatSlide lang={lang} />
|
||||
case 'annex-roadmap-2027':
|
||||
return <AnnexRoadmap2027Slide lang={lang} />
|
||||
case 'annex-roadmap-2028':
|
||||
return <AnnexRoadmap2028Slide lang={lang} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -138,6 +184,14 @@ export default function PitchDeck({ lang, onToggleLanguage }: PitchDeckProps) {
|
||||
total={nav.totalSlides}
|
||||
/>
|
||||
|
||||
<ChatFAB
|
||||
lang={lang}
|
||||
currentSlide={nav.currentSlide}
|
||||
currentIndex={nav.currentIndex}
|
||||
visitedSlides={nav.visitedSlides}
|
||||
onGoToSlide={nav.goToSlide}
|
||||
/>
|
||||
|
||||
<NavigationFAB
|
||||
currentIndex={nav.currentIndex}
|
||||
totalSlides={nav.totalSlides}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { Language } from '@/lib/types'
|
||||
import { t } from '@/lib/i18n'
|
||||
@@ -29,26 +30,33 @@ export default function SlideOverview({ currentIndex, onGoToSlide, onClose, lang
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{i.slideNames.map((name, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.03 }}
|
||||
onClick={() => 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="text-xs font-mono text-white/40 block mb-1">{idx + 1}</span>
|
||||
<span className={`text-sm font-medium ${idx === currentIndex ? 'text-white' : 'text-white/70'}`}>
|
||||
{name}
|
||||
</span>
|
||||
</motion.button>
|
||||
<React.Fragment key={idx}>
|
||||
{idx === 14 && (
|
||||
<div className="col-span-full py-2">
|
||||
<span className="text-xs text-white/30 uppercase tracking-wider">Appendix</span>
|
||||
<div className="h-px bg-white/10 mt-1" />
|
||||
</div>
|
||||
)}
|
||||
<motion.button
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.03 }}
|
||||
onClick={() => 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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="text-xs font-mono text-white/40 block mb-1">{idx + 1}</span>
|
||||
<span className={`text-sm font-medium ${idx === currentIndex ? 'text-white' : 'text-white/70'}`}>
|
||||
{name}
|
||||
</span>
|
||||
</motion.button>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
200
pitch-deck/components/slides/AnnexAIStackSlide.tsx
Normal file
200
pitch-deck/components/slides/AnnexAIStackSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="h-screen w-full flex items-center justify-center bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white p-8">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'KI-Stack & LLM-Architektur' : 'AI Stack & LLM Architecture'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-slate-400">
|
||||
{lang === 'de' ? 'Von 32B zu 1000B Parametern' : 'From 32B to 1000B parameters'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* LLM Evolution Roadmap */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.6 }}
|
||||
className="mb-12"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mb-12">
|
||||
{roadmapSteps.map((step, index) => (
|
||||
<div key={step.year} className="flex items-center flex-1">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3 + index * 0.1, duration: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 flex-1"
|
||||
>
|
||||
<div className="text-sm text-blue-400 font-semibold mb-1">{step.year}</div>
|
||||
<div className="text-base font-bold mb-1">{step.model}</div>
|
||||
<div className="text-xs text-slate-400">{step.label}</div>
|
||||
</motion.div>
|
||||
|
||||
{index < roadmapSteps.length - 1 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.4 + index * 0.1, duration: 0.5 }}
|
||||
className="mx-2"
|
||||
>
|
||||
<ArrowRight className="w-5 h-5 text-blue-400" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom Section: 2-column grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Left Card - Multi-Model Router */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.6, duration: 0.6 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="bg-blue-500/20 p-3 rounded-lg">
|
||||
<Brain className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">
|
||||
{lang === 'de' ? 'Multi-Model Router' : 'Multi-Model Router'}
|
||||
</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-slate-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Automatische Modellauswahl basierend auf Aufgabe'
|
||||
: 'Automatic model selection based on task'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Einfache Anfragen → kleineres Modell (schnell, günstig)'
|
||||
: 'Simple queries → smaller model (fast, cheap)'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-blue-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Komplexe Analysen → größeres Modell (präzise)'
|
||||
: 'Complex analysis → larger model (accurate)'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-400 mt-1">•</span>
|
||||
<span className="font-semibold text-green-400">
|
||||
{lang === 'de'
|
||||
? 'Kostenoptimierung: 60% günstiger als immer großes Modell'
|
||||
: 'Cost optimization: 60% cheaper than always using large model'}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Card - Inference Optimization */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.7, duration: 0.6 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="bg-purple-500/20 p-3 rounded-lg">
|
||||
<Zap className="w-6 h-6 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">
|
||||
{lang === 'de' ? 'Inference-Optimierung' : 'Inference Optimization'}
|
||||
</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-slate-300">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Apple Neural Engine Beschleunigung'
|
||||
: 'Apple Neural Engine acceleration'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Quantisierung (INT4/INT8) für schnellere Inferenz'
|
||||
: 'Quantization (INT4/INT8) for faster inference'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-1">•</span>
|
||||
<span>Context window: 32k tokens</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-purple-400 mt-1">•</span>
|
||||
<span>
|
||||
{lang === 'de'
|
||||
? 'Durchschn. Antwortzeit: <2s für einfache Anfragen'
|
||||
: 'Avg response time: <2s for simple queries'}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-green-400 mt-1">•</span>
|
||||
<span className="font-semibold text-green-400">
|
||||
{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'}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
147
pitch-deck/components/slides/AnnexAgentArchSlide.tsx
Normal file
147
pitch-deck/components/slides/AnnexAgentArchSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Compliance Agent Architektur' : 'Compliance Agent Architecture'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de' ? 'Multi-Agent System für autonome Compliance' : 'Multi-agent system for autonomous compliance'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Agent Architecture Diagram */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="mb-12 grid grid-cols-3 grid-rows-3 gap-4 max-w-4xl mx-auto">
|
||||
{/* Top agents */}
|
||||
{agents.slice(0, 2).map((agent, idx) => (
|
||||
<motion.div
|
||||
key={agent.name}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3 + idx * 0.1 }}
|
||||
className={`${agent.position} bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 text-center`}
|
||||
>
|
||||
<agent.icon className={`w-10 h-10 mx-auto mb-2 ${agent.color}`} />
|
||||
<h3 className="text-sm font-semibold text-white/90">{agent.name}</h3>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{/* Center Orchestrator */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="col-start-2 row-start-2 bg-gradient-to-br from-indigo-500/20 to-purple-500/20 border-2 border-indigo-400/30 rounded-xl p-6 text-center relative"
|
||||
>
|
||||
<Brain className="w-12 h-12 mx-auto mb-2 text-indigo-400" />
|
||||
<h3 className="text-lg font-bold text-white">Orchestrator</h3>
|
||||
<p className="text-xs text-white/60 mt-1">
|
||||
{lang === 'de' ? 'Koordiniert alle Agents' : 'Coordinates all agents'}
|
||||
</p>
|
||||
|
||||
{/* Connection lines */}
|
||||
<div className="absolute -top-4 left-1/2 w-0.5 h-4 bg-indigo-400/30" />
|
||||
<div className="absolute -bottom-4 left-1/2 w-0.5 h-4 bg-indigo-400/30" />
|
||||
<div className="absolute -left-4 top-1/2 h-0.5 w-4 bg-indigo-400/30" />
|
||||
<div className="absolute -right-4 top-1/2 h-0.5 w-4 bg-indigo-400/30" />
|
||||
</motion.div>
|
||||
|
||||
{/* Bottom agents */}
|
||||
{agents.slice(2, 4).map((agent, idx) => (
|
||||
<motion.div
|
||||
key={agent.name}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.6 + idx * 0.1 }}
|
||||
className={`${agent.position} bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 text-center`}
|
||||
>
|
||||
<agent.icon className={`w-10 h-10 mx-auto mb-2 ${agent.color}`} />
|
||||
<h3 className="text-sm font-semibold text-white/90">{agent.name}</h3>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Features */}
|
||||
<FadeInView delay={0.8}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{features.map((feature, idx) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.9 + idx * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<feature.icon className="w-8 h-8 text-indigo-400 mb-3" />
|
||||
<h3 className="text-lg font-semibold text-white/90 mb-2">{feature.title}</h3>
|
||||
<p className="text-sm text-white/60">{feature.desc}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
145
pitch-deck/components/slides/AnnexAgentRAGSlide.tsx
Normal file
145
pitch-deck/components/slides/AnnexAgentRAGSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Rechtsdokumente im RAG' : 'Legal Documents in RAG'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de' ? 'Wie Gesetze in die KI gelangen' : 'How laws enter the AI'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Regulations */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
|
||||
{regulations.map((reg, idx) => (
|
||||
<motion.div
|
||||
key={reg.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 + idx * 0.1 }}
|
||||
className={`bg-gradient-to-br ${reg.color} border ${reg.border} rounded-xl p-6 text-center`}
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-white mb-4">{reg.name}</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="bg-white/[0.08] rounded-lg p-3">
|
||||
<p className="text-lg font-semibold text-white/90">{reg.articles}</p>
|
||||
</div>
|
||||
<div className="bg-white/[0.08] rounded-lg p-3">
|
||||
<p className="text-lg font-semibold text-white/90">{reg.recitals}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Chunking Strategy */}
|
||||
<FadeInView delay={0.6}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-white/90 mb-6 text-center">
|
||||
{lang === 'de' ? 'Chunking-Strategie' : 'Chunking Strategy'}
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{chunkingDetails.map((detail, idx) => (
|
||||
<motion.div
|
||||
key={detail.label}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.8 + idx * 0.1 }}
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<detail.icon className="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white/80 mb-1">{detail.label}</h4>
|
||||
<p className="text-sm text-white/60">{detail.value}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
className="text-center pt-4 border-t border-white/[0.06]"
|
||||
>
|
||||
<p className="text-lg font-semibold text-indigo-400">
|
||||
{lang === 'de' ? 'Aktuell: 15.000+ Chunks indiziert' : 'Currently: 15,000+ chunks indexed'}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
162
pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx
Normal file
162
pitch-deck/components/slides/AnnexAgentWorkflowSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Autonomer Compliance-Workflow' : 'Autonomous Compliance Workflow'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de' ? 'End-to-End: Vom Upload bis zum fertigen Report' : 'End-to-end: From upload to finished report'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Workflow Steps */}
|
||||
<div className="lg:col-span-2">
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="space-y-4">
|
||||
{workflowSteps.map((step, idx) => (
|
||||
<motion.div
|
||||
key={step.title}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + idx * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 relative"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 bg-white/[0.06] rounded-lg ${step.color}`}>
|
||||
<step.icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-xs font-bold text-white/40">
|
||||
{lang === 'de' ? 'SCHRITT' : 'STEP'} {idx + 1}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white/90 mb-1">{step.title}</h3>
|
||||
<p className="text-sm text-white/60">{step.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connection line to next step */}
|
||||
{idx < workflowSteps.length - 1 && (
|
||||
<div className="absolute left-8 -bottom-4 w-0.5 h-4 bg-gradient-to-b from-white/[0.2] to-transparent" />
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
|
||||
{/* Human-in-the-Loop */}
|
||||
<div className="lg:col-span-1">
|
||||
<FadeInView delay={0.9}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.0 }}
|
||||
className="bg-gradient-to-br from-indigo-500/20 to-purple-500/20 border border-indigo-400/30 rounded-xl p-6 sticky top-8"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<ShieldCheck className="w-8 h-8 text-indigo-400" />
|
||||
<h3 className="text-xl font-bold text-white">
|
||||
{lang === 'de' ? 'Human-in-the-Loop' : 'Human-in-the-Loop'}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-4">
|
||||
{humanInLoopFeatures.map((feature, idx) => (
|
||||
<motion.li
|
||||
key={idx}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 1.1 + idx * 0.1 }}
|
||||
className="flex items-start gap-3"
|
||||
>
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-indigo-400 mt-2 flex-shrink-0" />
|
||||
<p className="text-sm text-white/80">{feature}</p>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.5 }}
|
||||
className="mt-6 pt-6 border-t border-white/[0.1]"
|
||||
>
|
||||
<p className="text-xs text-center text-white/60 italic">
|
||||
{lang === 'de'
|
||||
? 'KI automatisiert Routine, Mensch behält Kontrolle über kritische Entscheidungen'
|
||||
: 'AI automates routine, human retains control over critical decisions'}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
146
pitch-deck/components/slides/AnnexDevOpsSlide.tsx
Normal file
146
pitch-deck/components/slides/AnnexDevOpsSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'DevOps & CI/CD' : 'DevOps & CI/CD'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de'
|
||||
? 'Automatisierte Entwicklungs- und Deployment-Pipeline'
|
||||
: 'Automated development and deployment pipeline'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* CI/CD Pipeline Visualization */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6 mb-8"
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-6 text-white/90 text-center">
|
||||
{lang === 'de' ? 'CI/CD Pipeline' : 'CI/CD Pipeline'}
|
||||
</h3>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{pipelineSteps.map((step, index) => (
|
||||
<div key={step.name} className="flex items-center gap-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className={`w-16 h-16 rounded-lg bg-gradient-to-br ${step.color} flex items-center justify-center mb-2`}>
|
||||
<step.icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<span className="text-xs text-white/70 text-center">{step.name}</span>
|
||||
</motion.div>
|
||||
{index < pipelineSteps.length - 1 && (
|
||||
<ArrowRight className="w-5 h-5 text-white/30 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* DevOps Tools Grid */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{devOpsTools.map((tool, index) => (
|
||||
<motion.div
|
||||
key={tool.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8 + index * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-lg bg-gradient-to-br ${tool.accentColor} flex items-center justify-center mb-4`}>
|
||||
<tool.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-4 text-white">{tool.title}</h3>
|
||||
<ul className="space-y-2">
|
||||
{tool.items.map((item, itemIndex) => (
|
||||
<li key={itemIndex} className="text-sm text-white/70 flex items-start">
|
||||
<span className="mr-2 text-white/40">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
pitch-deck/components/slides/AnnexInfraSlide.tsx
Normal file
97
pitch-deck/components/slides/AnnexInfraSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView delay={0}>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-center mb-4">
|
||||
<GradientText>{title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-center text-white/60 mb-12">{subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{sections.map((section, idx) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 + idx * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="p-2 bg-purple-500/10 rounded-lg">
|
||||
<Icon className="w-6 h-6 text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-white mt-1">{section.title}</h3>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{section.items.map((item, itemIdx) => (
|
||||
<li key={itemIdx} className="text-white/70 text-sm leading-relaxed">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
204
pitch-deck/components/slides/AnnexRAGSlide.tsx
Normal file
204
pitch-deck/components/slides/AnnexRAGSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'RAG Pipeline & Vektordatenbank' : 'RAG Pipeline & Vector Database'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de'
|
||||
? 'Präzise Antworten durch intelligentes Retrieval'
|
||||
: 'Precise answers through intelligent retrieval'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Pipeline Visualization */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="mb-12">
|
||||
<div className="relative flex items-center justify-between gap-2">
|
||||
{pipelineSteps.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={index} className="flex items-center flex-1">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.3 + index * 0.1 }}
|
||||
className="flex-1"
|
||||
>
|
||||
<div className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 text-center hover:bg-white/[0.06] transition-all duration-300">
|
||||
<Icon className="w-8 h-8 mx-auto mb-2 text-blue-400" />
|
||||
<p className="text-sm text-white/80">
|
||||
{lang === 'de' ? step.labelDE : step.labelEN}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{index < pipelineSteps.length - 1 && (
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ delay: 0.4 + index * 0.1, duration: 0.3 }}
|
||||
className="flex-shrink-0 mx-2 origin-left"
|
||||
>
|
||||
<div className="h-0.5 w-8 bg-gradient-to-r from-blue-500 to-purple-500" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Feature Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 + index * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 hover:bg-white/[0.06] transition-all duration-300"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-blue-500/10 rounded-lg">
|
||||
<Icon className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{lang === 'de' ? feature.titleDE : feature.titleEN}
|
||||
</h3>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{(lang === 'de' ? feature.pointsDE : feature.pointsEN).map((point, idx) => (
|
||||
<li key={idx} className="text-sm text-white/70 flex items-start">
|
||||
<span className="text-blue-400 mr-2">•</span>
|
||||
<span>{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx
Normal file
142
pitch-deck/components/slides/AnnexRoadmap2027Slide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Roadmap 2027' : 'Roadmap 2027'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-400">
|
||||
{lang === 'de'
|
||||
? 'Product-Market Fit & Series A Vorbereitung'
|
||||
: 'Product-Market Fit & Series A Preparation'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{quarters.map((quarter, index) => (
|
||||
<motion.div
|
||||
key={quarter.quarter}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<div className={`bg-gradient-to-r ${quarter.accent} text-white text-sm font-bold px-3 py-1 rounded-lg inline-block mb-4`}>
|
||||
{quarter.quarter}
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{quarter.items.map((item, i) => (
|
||||
<li key={i} className="text-sm text-gray-300 flex items-start">
|
||||
<span className="text-blue-400 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{metrics.map((metric, index) => {
|
||||
const Icon = metric.icon
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<Icon className="w-8 h-8 text-blue-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400 mb-1">{metric.label}</p>
|
||||
<p className="text-2xl font-bold text-white">{metric.value}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx
Normal file
142
pitch-deck/components/slides/AnnexRoadmap2028Slide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Roadmap 2028' : 'Roadmap 2028'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-gray-400">
|
||||
{lang === 'de'
|
||||
? 'Enterprise Scale & Series A'
|
||||
: 'Enterprise Scale & Series A'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{quarters.map((quarter, index) => (
|
||||
<motion.div
|
||||
key={quarter.quarter}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<div className={`bg-gradient-to-r ${quarter.accent} text-white text-sm font-bold px-3 py-1 rounded-lg inline-block mb-4`}>
|
||||
{quarter.quarter}
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{quarter.items.map((item, i) => (
|
||||
<li key={i} className="text-sm text-gray-300 flex items-start">
|
||||
<span className="text-blue-400 mr-2">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{metrics.map((metric, index) => {
|
||||
const Icon = metric.icon
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<Icon className="w-8 h-8 text-blue-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400 mb-1">{metric.label}</p>
|
||||
<p className="text-2xl font-bold text-white">{metric.value}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
130
pitch-deck/components/slides/AnnexSecuritySlide.tsx
Normal file
130
pitch-deck/components/slides/AnnexSecuritySlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Sicherheitsarchitektur' : 'Security Architecture'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de'
|
||||
? 'Enterprise-Grade Sicherheit auf eigener Hardware'
|
||||
: 'Enterprise-grade security on own hardware'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Security Features Grid */}
|
||||
<div className="grid md:grid-cols-3 gap-6 mb-8">
|
||||
{securityFeatures.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4"
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-lg bg-gradient-to-br ${feature.accentColor} flex items-center justify-center mb-4`}>
|
||||
<feature.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-4 text-white">{feature.title}</h3>
|
||||
<ul className="space-y-2">
|
||||
{feature.items.map((item, itemIndex) => (
|
||||
<li key={itemIndex} className="text-sm text-white/70 flex items-start">
|
||||
<span className="mr-2 text-white/40">•</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Security Tools Bar */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6"
|
||||
>
|
||||
<h3 className="text-lg font-semibold mb-4 text-white/90">
|
||||
{lang === 'de' ? 'Sicherheitstools' : 'Security Tools'}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{securityTools.map((tool, index) => (
|
||||
<motion.div
|
||||
key={tool.name}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5 + index * 0.05 }}
|
||||
className="bg-white/[0.06] border border-white/[0.08] rounded-lg px-4 py-2 flex items-center gap-2"
|
||||
>
|
||||
<tool.icon className="w-4 h-4 text-white/60" />
|
||||
<div>
|
||||
<span className="text-sm font-medium text-white">{tool.name}</span>
|
||||
<span className="text-xs text-white/50 ml-2">{tool.description}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
181
pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx
Normal file
181
pitch-deck/components/slides/AnnexUSPComparisonSlide.tsx
Normal file
@@ -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 <Check className="w-5 h-5 text-green-500" />
|
||||
} else if (status === 'no') {
|
||||
return <X className="w-5 h-5 text-white/40" />
|
||||
} else if (status === 'partial') {
|
||||
return <Minus className="w-5 h-5 text-amber-500" />
|
||||
}
|
||||
return <span className="text-sm text-white/80">{status}</span>
|
||||
}
|
||||
|
||||
export default function AnnexUSPComparisonSlide({ lang }: AnnexUSPComparisonSlideProps) {
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Detailvergleich Wettbewerb' : 'Detailed Competitor Comparison'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de' ? 'BreakPilot vs. etablierte Anbieter' : 'BreakPilot vs. established providers'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl overflow-hidden"
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06]">
|
||||
<th className="text-left p-4 text-white/60 font-medium"></th>
|
||||
<th className="text-center p-4 bg-indigo-500/10 border-x border-indigo-500/20">
|
||||
<div className="text-lg font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">
|
||||
BreakPilot
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-center p-4 text-white font-medium">Proliance</th>
|
||||
<th className="text-center p-4 text-white font-medium">DataGuard</th>
|
||||
<th className="text-center p-4 text-white font-medium">heyData</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonData.map((row, index) => (
|
||||
<motion.tr
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05, duration: 0.3 }}
|
||||
className="border-b border-white/[0.06] hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<td className="p-4 text-white/80 font-medium">
|
||||
{lang === 'de' ? row.featureDE : row.featureEN}
|
||||
</td>
|
||||
<td className="p-4 text-center bg-indigo-500/5 border-x border-indigo-500/10">
|
||||
<div className="flex justify-center">
|
||||
<StatusIcon status={row.breakpilot} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="flex justify-center">
|
||||
<StatusIcon status={row.proliance} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="flex justify-center">
|
||||
<StatusIcon status={row.dataguard} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="flex justify-center">
|
||||
<StatusIcon status={row.heydata} />
|
||||
</div>
|
||||
</td>
|
||||
</motion.tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.6, duration: 0.5 }}
|
||||
className="mt-8 text-center"
|
||||
>
|
||||
<p className="text-sm text-white/50">
|
||||
{lang === 'de'
|
||||
? 'Stand: Februar 2026. Preise und Features der Wettbewerber koennen variieren.'
|
||||
: 'As of: February 2026. Competitor prices and features may vary.'}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
pitch-deck/components/slides/AnnexUSPMoatSlide.tsx
Normal file
127
pitch-deck/components/slides/AnnexUSPMoatSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Verteidigbare Marktposition' : 'Defensible Market Position'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de'
|
||||
? 'Fuenf Verteidigungslinien gegen Wettbewerber'
|
||||
: 'Five lines of defense against competitors'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="space-y-6">
|
||||
{moats.map((moat, index) => {
|
||||
const Icon = moat.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-6 flex items-center gap-6 hover:bg-white/[0.06] transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className={`w-16 h-16 rounded-full bg-gradient-to-r ${moat.gradient} flex items-center justify-center relative`}>
|
||||
<div className="absolute -top-2 -left-2 w-8 h-8 rounded-full bg-white/10 backdrop-blur-sm flex items-center justify-center border border-white/20">
|
||||
<span className="text-xs font-bold text-white">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<Icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold text-white mb-2">
|
||||
{lang === 'de' ? moat.titleDE : moat.titleEN}
|
||||
</h3>
|
||||
<p className="text-white/70 leading-relaxed">
|
||||
{lang === 'de' ? moat.descDE : moat.descEN}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={`w-1 h-16 rounded-full bg-gradient-to-b ${moat.gradient} flex-shrink-0`} />
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8, duration: 0.5 }}
|
||||
className="mt-12 text-center"
|
||||
>
|
||||
<div className="bg-gradient-to-r from-indigo-500/10 to-purple-500/10 border border-indigo-500/20 rounded-xl p-6">
|
||||
<p className="text-white/80 text-lg font-medium">
|
||||
{lang === 'de'
|
||||
? 'Kombination aus physischen und digitalen Barriers macht Marktposition langfristig verteidigbar'
|
||||
: 'Combination of physical and digital barriers makes market position defensible long-term'}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
129
pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx
Normal file
129
pitch-deck/components/slides/AnnexUSPOverviewSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView>
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-4">
|
||||
<GradientText>
|
||||
{lang === 'de' ? 'Unsere 5 USPs' : 'Our 5 USPs'}
|
||||
</GradientText>
|
||||
</h2>
|
||||
<p className="text-xl text-white/60">
|
||||
{lang === 'de' ? 'Was uns einzigartig macht' : 'What makes us unique'}
|
||||
</p>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
{usps.slice(0, 3).map((usp, index) => {
|
||||
const Icon = usp.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 relative overflow-hidden"
|
||||
>
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${usp.gradient}`} />
|
||||
<div className="flex flex-col items-center text-center pt-2">
|
||||
<div className={`w-16 h-16 rounded-full bg-gradient-to-r ${usp.gradient} flex items-center justify-center mb-4`}>
|
||||
<Icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-3">
|
||||
{lang === 'de' ? usp.titleDE : usp.titleEN}
|
||||
</h3>
|
||||
<p className="text-sm text-white/70 leading-relaxed">
|
||||
{lang === 'de' ? usp.descDE : usp.descEN}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||
{usps.slice(3, 5).map((usp, index) => {
|
||||
const Icon = usp.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={index + 3}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: (index + 3) * 0.1, duration: 0.5 }}
|
||||
className="bg-white/[0.04] border border-white/[0.06] rounded-xl p-4 relative overflow-hidden"
|
||||
>
|
||||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${usp.gradient}`} />
|
||||
<div className="flex flex-col items-center text-center pt-2">
|
||||
<div className={`w-16 h-16 rounded-full bg-gradient-to-r ${usp.gradient} flex items-center justify-center mb-4`}>
|
||||
<Icon className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-white mb-3">
|
||||
{lang === 'de' ? usp.titleDE : usp.titleEN}
|
||||
</h3>
|
||||
<p className="text-sm text-white/70 leading-relaxed">
|
||||
{lang === 'de' ? usp.descDE : usp.descEN}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
68
pitch-deck/components/slides/AppendixSlide.tsx
Normal file
68
pitch-deck/components/slides/AppendixSlide.tsx
Normal file
@@ -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 (
|
||||
<div className="h-full flex items-center justify-center px-16">
|
||||
<FadeInView className="text-center max-w-4xl">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<h1 className="text-7xl font-bold mb-8">
|
||||
<GradientText>{t.title}</GradientText>
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
className="text-2xl text-white/80 mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
{t.subtitle}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="w-full h-px bg-gradient-to-r from-transparent via-white/20 to-transparent mb-8"
|
||||
initial={{ scaleX: 0, opacity: 0 }}
|
||||
animate={{ scaleX: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
/>
|
||||
|
||||
<motion.p
|
||||
className="text-sm text-white/50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.8 }}
|
||||
>
|
||||
{t.detailInfo}
|
||||
</motion.p>
|
||||
</FadeInView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
<div className="mb-6">
|
||||
<h3 className="text-sm font-semibold text-indigo-400 mb-3 flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
{lang === 'de' ? 'Integrierte Security & Developer Tools — nur bei ComplAI' : 'Integrated Security & Developer Tools — ComplAI only'}
|
||||
{lang === 'de' ? <>Integrierte Security & Developer Tools — nur bei <BrandName /></> : <>Integrated Security & Developer Tools — <BrandName /> only</>}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{secFeats.map((feat, idx) => {
|
||||
|
||||
@@ -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{' '}
|
||||
<GradientText className="text-5xl md:text-7xl font-bold" delay={0.5}>
|
||||
ComplAI
|
||||
</GradientText>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
>
|
||||
<BrandName className="text-5xl md:text-7xl font-bold" />
|
||||
</motion.span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Tagline */}
|
||||
|
||||
@@ -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<FinTab>('overview')
|
||||
const [accountingStandard, setAccountingStandard] = useState<AccountingStandard>('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<string, string> = {}
|
||||
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 (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView className="text-center mb-3">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-1">
|
||||
<GradientText>{i.financials.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.financials.subtitle}</p>
|
||||
<p className="text-sm text-white/50 max-w-2xl mx-auto">{i.financials.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Key Numbers */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<GlassCard delay={0.2} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.arr} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={Math.round((lastYear?.arr_eur || 0) * growthMultiplier / 1_000_000)}
|
||||
suffix={lang === 'de' ? ' Mio.' : 'M'}
|
||||
duration={1200}
|
||||
/>
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.25} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.customers} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter target={Math.round((lastYear?.customers_count || 0) * growthMultiplier)} duration={1200} />
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.3} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.employees} 2030</p>
|
||||
<p className="text-xl font-bold text-white">{lastYear?.employees_count || 0}</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.35} className="p-4 text-center">
|
||||
<p className="text-xs text-white/40 mb-1">{i.financials.mrr} 2030</p>
|
||||
<p className="text-xl font-bold text-white">
|
||||
<AnimatedCounter
|
||||
target={Math.round((lastYear?.mrr_eur || 0) * growthMultiplier / 1_000)}
|
||||
suffix="k"
|
||||
duration={1200}
|
||||
/>
|
||||
</p>
|
||||
</GlassCard>
|
||||
{/* Hero KPI Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mb-3">
|
||||
<KPICard
|
||||
label={`ARR 2030`}
|
||||
value={summary ? Math.round(summary.final_arr / 1_000_000 * 10) / 10 : 0}
|
||||
suffix=" Mio."
|
||||
decimals={1}
|
||||
trend="up"
|
||||
color="#6366f1"
|
||||
delay={0.1}
|
||||
subLabel="EUR"
|
||||
/>
|
||||
<KPICard
|
||||
label={de ? 'Kunden 2030' : 'Customers 2030'}
|
||||
value={summary?.final_customers || 0}
|
||||
trend="up"
|
||||
color="#22c55e"
|
||||
delay={0.15}
|
||||
/>
|
||||
<KPICard
|
||||
label="Break-Even"
|
||||
value={summary?.break_even_month || 0}
|
||||
suffix={de ? ' Mo.' : ' mo.'}
|
||||
trend={summary?.break_even_month && summary.break_even_month <= 24 ? 'up' : 'down'}
|
||||
color="#eab308"
|
||||
delay={0.2}
|
||||
subLabel={summary?.break_even_month ? `~${Math.ceil((summary.break_even_month) / 12) + 2025}` : ''}
|
||||
/>
|
||||
<KPICard
|
||||
label="LTV/CAC"
|
||||
value={summary?.final_ltv_cac || 0}
|
||||
suffix="x"
|
||||
decimals={1}
|
||||
trend={(summary?.final_ltv_cac || 0) >= 3 ? 'up' : 'down'}
|
||||
color="#a855f7"
|
||||
delay={0.25}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Chart */}
|
||||
<FadeInView delay={0.4} className="md:col-span-2">
|
||||
<GlassCard hover={false} className="p-4">
|
||||
<FinancialChart financials={financials} lang={lang} growthMultiplier={growthMultiplier} />
|
||||
</GlassCard>
|
||||
</FadeInView>
|
||||
{/* Tab Navigation + Accounting Standard Toggle */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-1">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs transition-all
|
||||
${activeTab === tab.id
|
||||
? 'bg-indigo-500/20 text-indigo-300 border border-indigo-500/30'
|
||||
: 'bg-white/[0.04] text-white/40 border border-transparent hover:text-white/60 hover:bg-white/[0.06]'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sliders */}
|
||||
<FadeInView delay={0.5}>
|
||||
<FinancialSliders
|
||||
growthRate={growthRate}
|
||||
churnRate={churnRate}
|
||||
arpu={arpu}
|
||||
onGrowthChange={setGrowthRate}
|
||||
onChurnChange={setChurnRate}
|
||||
onArpuChange={setArpu}
|
||||
lang={lang}
|
||||
/>
|
||||
</FadeInView>
|
||||
{/* HGB / US GAAP Toggle — only visible on GuV and Cashflow tabs */}
|
||||
{(activeTab === 'guv' || activeTab === 'cashflow') && (
|
||||
<div className="flex items-center gap-1 bg-white/[0.04] rounded-lg p-0.5 border border-white/[0.06]">
|
||||
<button
|
||||
onClick={() => setAccountingStandard('hgb')}
|
||||
className={`px-2.5 py-1 rounded-md text-[10px] font-medium transition-all
|
||||
${accountingStandard === 'hgb'
|
||||
? 'bg-indigo-500/25 text-indigo-300 shadow-sm'
|
||||
: 'text-white/40 hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
HGB
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setAccountingStandard('usgaap')}
|
||||
className={`px-2.5 py-1 rounded-md text-[10px] font-medium transition-all
|
||||
${accountingStandard === 'usgaap'
|
||||
? 'bg-indigo-500/25 text-indigo-300 shadow-sm'
|
||||
: 'text-white/40 hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
US GAAP
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main content: 3-column layout */}
|
||||
<div className="grid md:grid-cols-12 gap-3">
|
||||
{/* Left: Charts (8 columns) */}
|
||||
<div className="md:col-span-8 space-y-3">
|
||||
|
||||
{/* TAB: Overview — monatlicher Chart + Waterfall + Unit Economics */}
|
||||
{activeTab === 'overview' && (
|
||||
<>
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-xs text-white/40">
|
||||
{de ? 'Umsatz vs. Kosten (60 Monate)' : 'Revenue vs. Costs (60 months)'}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-[9px]">
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-indigo-500 inline-block" /> {de ? 'Umsatz' : 'Revenue'}</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-red-400 inline-block" style={{ borderBottom: '1px dashed' }} /> {de ? 'Kosten' : 'Costs'}</span>
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-0.5 bg-emerald-500 inline-block" /> {de ? 'Kunden' : 'Customers'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<FinancialChart
|
||||
activeResults={activeResults}
|
||||
compareResults={compareResults}
|
||||
compareMode={fm.compareMode}
|
||||
scenarioColors={scenarioColors}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
||||
<p className="text-xs text-white/40 mb-2">
|
||||
{de ? 'Cash-Flow (Quartal)' : 'Cash Flow (Quarterly)'}
|
||||
</p>
|
||||
{activeResults && <WaterfallChart results={activeResults.results} lang={lang} />}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
<FadeInView delay={0.25}>
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3 flex justify-center">
|
||||
<RunwayGauge
|
||||
months={lastResult?.runway_months || 0}
|
||||
size={120}
|
||||
label={de ? 'Runway (Monate)' : 'Runway (months)'}
|
||||
/>
|
||||
</div>
|
||||
{lastResult && (
|
||||
<UnitEconomicsCards
|
||||
cac={lastResult.cac_eur}
|
||||
ltv={lastResult.ltv_eur}
|
||||
ltvCacRatio={lastResult.ltv_cac_ratio}
|
||||
grossMargin={lastResult.gross_margin_pct}
|
||||
churnRate={fm.activeScenario?.assumptions.find(a => a.key === 'churn_rate_monthly')?.value as number || 3}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TAB: GuV — Annual P&L Table */}
|
||||
{activeTab === 'guv' && activeResults && (
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-xs text-white/40">
|
||||
{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)')
|
||||
}
|
||||
</p>
|
||||
<p className="text-[9px] text-white/20">
|
||||
{de ? 'Alle Werte in EUR' : 'All values in EUR'}
|
||||
</p>
|
||||
</div>
|
||||
<AnnualPLTable results={activeResults.results} lang={lang} standard={accountingStandard} />
|
||||
</div>
|
||||
</FadeInView>
|
||||
)}
|
||||
|
||||
{/* TAB: Cashflow & Finanzbedarf */}
|
||||
{activeTab === 'cashflow' && activeResults && (
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-4">
|
||||
<p className="text-xs text-white/40 mb-3">
|
||||
{accountingStandard === 'hgb'
|
||||
? (de ? 'Kapitalflussrechnung (HGB)' : 'Cash Flow Statement (HGB)')
|
||||
: (de ? 'Cash Flow Statement (US GAAP)' : 'Cash Flow Statement (US GAAP)')
|
||||
}
|
||||
</p>
|
||||
<AnnualCashflowChart
|
||||
results={activeResults.results}
|
||||
initialFunding={initialFunding}
|
||||
lang={lang}
|
||||
standard={accountingStandard}
|
||||
/>
|
||||
</div>
|
||||
</FadeInView>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Controls (4 columns) */}
|
||||
<div className="md:col-span-4 space-y-3">
|
||||
{/* Scenario Switcher */}
|
||||
<FadeInView delay={0.15}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
||||
<ScenarioSwitcher
|
||||
scenarios={fm.scenarios}
|
||||
activeId={fm.activeScenarioId}
|
||||
compareMode={fm.compareMode}
|
||||
onSelect={(id) => {
|
||||
fm.setActiveScenarioId(id)
|
||||
}}
|
||||
onToggleCompare={() => {
|
||||
if (!fm.compareMode) {
|
||||
fm.computeAll()
|
||||
}
|
||||
fm.setCompareMode(!fm.compareMode)
|
||||
}}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Assumption Sliders */}
|
||||
<FadeInView delay={0.2}>
|
||||
<div className="bg-white/[0.05] backdrop-blur-xl border border-white/10 rounded-2xl p-3">
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider mb-2">
|
||||
{i.financials.adjustAssumptions}
|
||||
</p>
|
||||
{fm.activeScenario && (
|
||||
<FinancialSliders
|
||||
assumptions={fm.activeScenario.assumptions}
|
||||
onAssumptionChange={(key, value) => {
|
||||
if (fm.activeScenarioId) {
|
||||
fm.updateAssumption(fm.activeScenarioId, key, value)
|
||||
}
|
||||
}}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
{fm.computing && (
|
||||
<div className="flex items-center gap-2 mt-2 text-[10px] text-indigo-400">
|
||||
<div className="w-3 h-3 border border-indigo-400 border-t-transparent rounded-full animate-spin" />
|
||||
{de ? 'Berechne...' : 'Computing...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.solution.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">{i.solution.subtitle}</p>
|
||||
<p className="text-lg text-white/50 max-w-2xl mx-auto">
|
||||
<BrandName /> — {lang === 'de' ? 'Compliance auf Autopilot' : 'Compliance on Autopilot'}
|
||||
</p>
|
||||
</FadeInView>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
|
||||
125
pitch-deck/components/slides/TechnologySlide.tsx
Normal file
125
pitch-deck/components/slides/TechnologySlide.tsx
Normal file
@@ -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 (
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<FadeInView className="text-center mb-6">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-2">
|
||||
<GradientText>{i.technology.title}</GradientText>
|
||||
</h2>
|
||||
<p className="text-sm text-white/50 max-w-2xl mx-auto">{i.technology.subtitle}</p>
|
||||
</FadeInView>
|
||||
|
||||
{/* Timeline Section */}
|
||||
<FadeInView delay={0.1}>
|
||||
<div className="mb-6">
|
||||
<p className="text-[10px] text-white/30 uppercase tracking-wider mb-3">
|
||||
{i.technology.timelineTitle}
|
||||
</p>
|
||||
|
||||
{/* Timeline Line */}
|
||||
<div className="relative">
|
||||
<div className="absolute top-4 left-0 right-0 h-px bg-gradient-to-r from-blue-500/40 via-purple-500/40 to-fuchsia-500/40" />
|
||||
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{i.technology.phases.map((phase: any, idx: number) => (
|
||||
<motion.div
|
||||
key={phase.year}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 + idx * 0.1, duration: 0.5 }}
|
||||
className="relative"
|
||||
>
|
||||
{/* Dot on timeline */}
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className={`w-2.5 h-2.5 rounded-full ${phaseAccents[idx].replace('text-', 'bg-')} ring-2 ring-slate-950 relative z-10`} />
|
||||
</div>
|
||||
|
||||
{/* Phase Card */}
|
||||
<div className={`bg-gradient-to-b ${phaseColors[idx]} border rounded-xl p-2.5 backdrop-blur-sm`}>
|
||||
<p className={`text-xs font-bold ${phaseAccents[idx]} mb-0.5`}>{phase.year}</p>
|
||||
<p className="text-[10px] font-semibold text-white mb-1.5">{phase.phase}</p>
|
||||
<div className="space-y-0.5">
|
||||
{phase.techs.map((tech: string, i: number) => (
|
||||
<p key={i} className="text-[9px] text-white/40 leading-tight">
|
||||
{tech}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Tech Stack Layers */}
|
||||
<FadeInView delay={0.4}>
|
||||
<p className="text-[10px] text-white/30 uppercase tracking-wider mb-2">
|
||||
{i.technology.stackTitle}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
{i.technology.layers.map((layer: any, idx: number) => {
|
||||
const Icon = layerIcons[idx]
|
||||
return (
|
||||
<motion.div
|
||||
key={layer.name}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 + idx * 0.08, duration: 0.4 }}
|
||||
className={`flex items-center gap-3 bg-white/[0.04] backdrop-blur-sm border border-white/[0.06] rounded-xl px-3 py-2`}
|
||||
>
|
||||
<div className={`w-8 h-8 rounded-lg border flex items-center justify-center shrink-0 ${layerColors[idx]}`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<p className="text-[11px] font-semibold text-white/80 w-24 shrink-0">{layer.name}</p>
|
||||
<p className="text-[10px] text-white/40 truncate">{layer.techs}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<FadeInView className="text-center mb-10">
|
||||
<FadeInView className="text-center mb-8">
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-3">
|
||||
<GradientText>{i.theAsk.title}</GradientText>
|
||||
</h2>
|
||||
@@ -36,53 +81,93 @@ export default function TheAskSlide({ lang, funding }: TheAskSlideProps) {
|
||||
</FadeInView>
|
||||
|
||||
{/* Main Number */}
|
||||
<FadeInView delay={0.2} className="text-center mb-10">
|
||||
<FadeInView delay={0.2} className="text-center mb-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4, type: 'spring', stiffness: 200 }}
|
||||
>
|
||||
<p className="text-6xl md:text-8xl font-bold text-white mb-2">
|
||||
<AnimatedCounter target={200} suffix="k" duration={2000} />
|
||||
<span className="text-3xl md:text-4xl text-white/50 ml-2">EUR</span>
|
||||
<p className="text-5xl md:text-7xl font-bold text-white mb-1">
|
||||
<AnimatedCounter target={1.25} suffix="M" duration={2000} decimals={2} />
|
||||
<span className="text-2xl md:text-3xl text-white/50 ml-2">EUR</span>
|
||||
</p>
|
||||
<p className="text-sm text-white/40">
|
||||
{lang === 'de' ? 'Gestaffelte Finanzierung 2026-2027' : 'Staged Funding 2026-2027'}
|
||||
</p>
|
||||
</motion.div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Details */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-8">
|
||||
<GlassCard delay={0.5} className="text-center p-5">
|
||||
<FileText className="w-6 h-6 text-indigo-400 mx-auto mb-2" />
|
||||
{/* Funding Timeline */}
|
||||
<FadeInView delay={0.4} className="mb-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{FUNDING_TIMELINE.map((event, idx) => {
|
||||
const Icon = event.icon
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 + idx * 0.15 }}
|
||||
>
|
||||
<GlassCard delay={0} className="p-4 text-center h-full">
|
||||
<Icon className="w-5 h-5 mx-auto mb-2" style={{ color: event.color }} />
|
||||
<p className="text-xs text-white/40 mb-1">{event.month}</p>
|
||||
<p className="text-lg font-bold text-white mb-0.5">
|
||||
{event.amount >= 1000000
|
||||
? `${(event.amount / 1000000).toFixed(0)}M`
|
||||
: `${(event.amount / 1000).toFixed(0)}k`}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-white/70">
|
||||
{lang === 'de' ? event.label_de : event.label_en}
|
||||
</p>
|
||||
<p className="text-[10px] text-white/30 mt-0.5">
|
||||
{lang === 'de' ? event.desc_de : event.desc_en}
|
||||
</p>
|
||||
</GlassCard>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FadeInView>
|
||||
|
||||
{/* Details Row */}
|
||||
<div className="grid md:grid-cols-3 gap-3 mb-6">
|
||||
<GlassCard delay={0.8} className="text-center p-4">
|
||||
<FileText className="w-5 h-5 text-indigo-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{i.theAsk.instrument}</p>
|
||||
<p className="text-lg font-bold text-white">{funding?.instrument || 'SAFE'}</p>
|
||||
<p className="text-sm font-bold text-white">{funding?.instrument || 'Stammkapital + Wandeldarlehen + Equity'}</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.6} className="text-center p-5">
|
||||
<Calendar className="w-6 h-6 text-purple-400 mx-auto mb-2" />
|
||||
<GlassCard delay={0.9} className="text-center p-4">
|
||||
<Calendar className="w-5 h-5 text-purple-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{i.theAsk.targetDate}</p>
|
||||
<p className="text-lg font-bold text-white">Q3 2026</p>
|
||||
<p className="text-sm font-bold text-white">
|
||||
{lang === 'de' ? 'Aug 2026 — Jul 2027' : 'Aug 2026 — Jul 2027'}
|
||||
</p>
|
||||
</GlassCard>
|
||||
<GlassCard delay={0.7} className="text-center p-5">
|
||||
<Target className="w-6 h-6 text-blue-400 mx-auto mb-2" />
|
||||
<GlassCard delay={1.0} className="text-center p-4">
|
||||
<Target className="w-5 h-5 text-blue-400 mx-auto mb-2" />
|
||||
<p className="text-xs text-white/40 mb-1">{lang === 'de' ? 'Runway' : 'Runway'}</p>
|
||||
<p className="text-lg font-bold text-white">18 {lang === 'de' ? 'Monate' : 'Months'}</p>
|
||||
<p className="text-sm font-bold text-white">36+ {lang === 'de' ? 'Monate' : 'Months'}</p>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* Use of Funds */}
|
||||
<FadeInView delay={0.8}>
|
||||
<GlassCard hover={false} className="p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 text-center">{i.theAsk.useOfFunds}</h3>
|
||||
<div className="flex flex-col md:flex-row items-center gap-8">
|
||||
<FadeInView delay={1.1}>
|
||||
<GlassCard hover={false} className="p-5">
|
||||
<h3 className="text-base font-semibold text-white mb-3 text-center">
|
||||
{i.theAsk.useOfFunds} (1,25 Mio. EUR)
|
||||
</h3>
|
||||
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||
{/* Pie Chart */}
|
||||
<div className="w-48 h-48">
|
||||
<div className="w-40 h-40">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={50}
|
||||
outerRadius={80}
|
||||
innerRadius={45}
|
||||
outerRadius={70}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
>
|
||||
@@ -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) {
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex-1 space-y-2">
|
||||
{useOfFunds.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: COLORS[idx] }} />
|
||||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: COLORS[idx] }} />
|
||||
<span className="flex-1 text-sm text-white/70">
|
||||
{lang === 'de' ? item.label_de : item.label_en}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-white">{item.percentage}%</span>
|
||||
<span className="text-xs text-white/30">
|
||||
{((funding.amount_eur * item.percentage) / 100).toLocaleString('de-DE')} EUR
|
||||
{((totalFunding * item.percentage) / 100).toLocaleString('de-DE')} EUR
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
205
pitch-deck/components/ui/AnnualCashflowChart.tsx
Normal file
205
pitch-deck/components/ui/AnnualCashflowChart.tsx
Normal file
@@ -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<number, FMResult[]>()
|
||||
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 (
|
||||
<div>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Startkapital' : 'Initial Funding'}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-white">{formatValue(initialFunding)} EUR</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need'}
|
||||
</p>
|
||||
<p className="text-sm font-bold text-amber-400">{formatValue(cumulativeNeed)} EUR</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-[9px] text-white/30 uppercase tracking-wider">
|
||||
{de ? 'Finanzierungsluecke' : 'Funding Gap'}
|
||||
</p>
|
||||
<p className={`text-sm font-bold ${totalFundingGap > 0 ? 'text-red-400' : 'text-emerald-400'}`}>
|
||||
{totalFundingGap > 0 ? formatValue(totalFundingGap) + ' EUR' : (de ? 'Gedeckt' : 'Covered')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="w-full h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<XAxis
|
||||
dataKey="year"
|
||||
stroke="rgba(255,255,255,0.3)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 11 }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
|
||||
tickFormatter={formatValue}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.95)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
}}
|
||||
formatter={(value: number, name: string) => {
|
||||
const labels: Record<string, string> = 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]
|
||||
}}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="rgba(255,255,255,0.2)" />
|
||||
|
||||
{/* Net Cashflow Bars */}
|
||||
<Bar dataKey="netCashflow" radius={[4, 4, 4, 4]} barSize={28}>
|
||||
{data.map((entry, i) => (
|
||||
<Cell
|
||||
key={i}
|
||||
fill={entry.netCashflow >= 0 ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.6)'}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
|
||||
{/* Cash Balance Line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cashBalance"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2.5}
|
||||
dot={{ r: 4, fill: '#6366f1', stroke: '#1e1b4b', strokeWidth: 2 }}
|
||||
/>
|
||||
|
||||
{/* Cumulative Funding Need Line */}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="cumulativeFundingNeed"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
dot={{ r: 3, fill: '#f59e0b' }}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex items-center justify-center gap-4 mt-2 text-[9px] text-white/40">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-3 h-2.5 rounded-sm bg-emerald-500/70 inline-block" />
|
||||
<span className="w-3 h-2.5 rounded-sm bg-red-500/60 inline-block" />
|
||||
{isUSGAAP ? 'Net Cash Flow' : (de ? 'Netto-Cashflow' : 'Net Cash Flow')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-4 h-0.5 bg-indigo-500 inline-block" />
|
||||
{isUSGAAP ? 'Cash Balance' : (de ? 'Cash-Bestand' : 'Cash Balance')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="w-4 h-0.5 inline-block" style={{ borderBottom: '2px dashed #f59e0b' }} />
|
||||
{isUSGAAP ? 'Cum. Funding Need' : (de ? 'Kum. Finanzbedarf' : 'Cum. Funding Need')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
232
pitch-deck/components/ui/AnnualPLTable.tsx
Normal file
232
pitch-deck/components/ui/AnnualPLTable.tsx
Normal file
@@ -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<number, FMResult[]>()
|
||||
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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="overflow-x-auto"
|
||||
>
|
||||
<table className="w-full text-[11px]">
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left py-2 pr-4 text-white/40 font-medium min-w-[200px]">
|
||||
{standard === 'hgb'
|
||||
? 'GuV-Position (HGB)'
|
||||
: 'P&L Line Item (US GAAP)'}
|
||||
</th>
|
||||
{rows.map(r => (
|
||||
<th key={r.year} className="text-right py-2 px-2 text-white/50 font-semibold min-w-[80px]">
|
||||
{r.year}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineItems.map((item, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
className={`${item.isSeparator ? 'border-t border-white/10' : ''}`}
|
||||
>
|
||||
<td className={`py-1.5 pr-4
|
||||
${item.isBold ? 'text-white font-semibold' : 'text-white/50'}
|
||||
${item.isPercent ? 'italic text-white/30' : ''}
|
||||
${item.isSubRow ? 'text-[10px] text-white/30' : ''}
|
||||
`}>
|
||||
{item.label}
|
||||
</td>
|
||||
{rows.map(r => {
|
||||
const val = item.getValue(r)
|
||||
return (
|
||||
<td
|
||||
key={r.year}
|
||||
className={`text-right py-1.5 px-2 font-mono
|
||||
${item.isBold ? 'font-semibold' : ''}
|
||||
${item.isPercent ? 'text-white/30 italic' : ''}
|
||||
${item.isSubRow ? 'text-[10px] text-white/25' : ''}
|
||||
${!item.isPercent && val < 0 ? 'text-red-400/80' : ''}
|
||||
${!item.isPercent && val > 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))
|
||||
}
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
19
pitch-deck/components/ui/BrandName.tsx
Normal file
19
pitch-deck/components/ui/BrandName.tsx
Normal file
@@ -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 (
|
||||
<span className={className}>
|
||||
{prefix && <>BreakPilot </>}
|
||||
Compl<span className="bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">AI</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
<thead>
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="text-left px-4 py-3 font-medium text-white/60">Feature</th>
|
||||
<th className="px-4 py-3 font-bold text-indigo-400">ComplAI</th>
|
||||
<th className="px-4 py-3 font-bold text-indigo-400"><BrandName /></th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">Proliance</th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">DataGuard</th>
|
||||
<th className="px-4 py-3 font-medium text-white/60">heyData</th>
|
||||
|
||||
@@ -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<string, FMComputeResponse>
|
||||
compareMode?: boolean
|
||||
scenarioColors?: Record<string, string>
|
||||
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 (
|
||||
<div className="w-full h-[300px] flex items-center justify-center text-white/30 text-sm">
|
||||
{lang === 'de' ? 'Lade Daten...' : 'Loading data...'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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<string, number | string> = {
|
||||
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 (
|
||||
<div className="w-full h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<ComposedChart data={data} margin={{ top: 10, right: 50, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.8} />
|
||||
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.2} />
|
||||
<linearGradient id="fmRevenueGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.6} />
|
||||
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
<linearGradient id="costGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#f43f5e" stopOpacity={0.6} />
|
||||
<stop offset="100%" stopColor="#f43f5e" stopOpacity={0.1} />
|
||||
<linearGradient id="fmCostGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#f43f5e" stopOpacity={0.4} />
|
||||
<stop offset="100%" stopColor="#f43f5e" stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="year" stroke="rgba(255,255,255,0.3)" tick={{ fill: 'rgba(255,255,255,0.5)', fontSize: 12 }} />
|
||||
<YAxis stroke="rgba(255,255,255,0.1)" tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 11 }} tickFormatter={formatValue} />
|
||||
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
stroke="rgba(255,255,255,0.2)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 9 }}
|
||||
interval={5}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="left"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
|
||||
tickFormatter={formatValue}
|
||||
/>
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
tick={{ fill: 'rgba(34,197,94,0.5)', fontSize: 10 }}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.9)',
|
||||
background: 'rgba(10, 10, 26, 0.95)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
fontSize: 11,
|
||||
backdropFilter: 'blur(12px)',
|
||||
}}
|
||||
formatter={(value: number, name: string) => {
|
||||
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'}
|
||||
/>
|
||||
<Area type="monotone" dataKey={i.financials.revenue} fill="url(#revenueGradient)" stroke="#6366f1" strokeWidth={2} />
|
||||
<Bar dataKey={i.financials.costs} fill="url(#costGradient)" radius={[4, 4, 0, 0]} barSize={30} />
|
||||
<Line type="monotone" dataKey={i.financials.customers} stroke="#22c55e" strokeWidth={2} dot={{ r: 4, fill: '#22c55e' }} yAxisId={0} />
|
||||
|
||||
{/* Break-even reference line */}
|
||||
{breakEvenMonth && (
|
||||
<ReferenceLine
|
||||
x={data[breakEvenMonth - 1]?.label}
|
||||
yAxisId="left"
|
||||
stroke="#22c55e"
|
||||
strokeDasharray="5 5"
|
||||
label={{
|
||||
value: 'Break-Even',
|
||||
fill: '#22c55e',
|
||||
fontSize: 10,
|
||||
position: 'insideTopRight',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Revenue area */}
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
fill="url(#fmRevenueGradient)"
|
||||
stroke="#6366f1"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
|
||||
{/* Cost area */}
|
||||
<Area
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey="costs"
|
||||
fill="url(#fmCostGradient)"
|
||||
stroke="#f43f5e"
|
||||
strokeWidth={1.5}
|
||||
strokeDasharray="4 4"
|
||||
/>
|
||||
|
||||
{/* Customers line */}
|
||||
<Line
|
||||
yAxisId="right"
|
||||
type="monotone"
|
||||
dataKey="customers"
|
||||
stroke="#22c55e"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
|
||||
{/* Compare mode: overlay other scenarios */}
|
||||
{compareMode && compareResults && Array.from(compareResults.entries()).map(([scenarioId]) => (
|
||||
<Line
|
||||
key={`rev_${scenarioId}`}
|
||||
yAxisId="left"
|
||||
type="monotone"
|
||||
dataKey={`revenue_${scenarioId}`}
|
||||
stroke={scenarioColors[scenarioId] || '#888'}
|
||||
strokeWidth={1.5}
|
||||
strokeOpacity={0.5}
|
||||
dot={false}
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Brush for zooming */}
|
||||
<Brush
|
||||
dataKey="label"
|
||||
height={20}
|
||||
stroke="rgba(99,102,241,0.4)"
|
||||
fill="rgba(0,0,0,0.3)"
|
||||
travellerWidth={8}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] text-white/50">{label}</p>
|
||||
<div className="flex gap-1.5">
|
||||
{steps.map((s: number, i: number) => (
|
||||
<div key={i} className="flex-1 text-center">
|
||||
<p className="text-[9px] text-white/30">Y{i + 1}</p>
|
||||
<p className="text-xs text-white font-mono">{s}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-white/60">{label}</span>
|
||||
<span className="font-mono text-white">{value}{unit}</span>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between text-[11px]">
|
||||
<span className="text-white/50">{label}</span>
|
||||
<span className="font-mono text-white">{value}{assumption.unit === 'EUR' ? ' EUR' : assumption.unit === '%' ? '%' : ` ${assumption.unit || ''}`}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
min={assumption.min_value ?? 0}
|
||||
max={assumption.max_value ?? 100}
|
||||
step={assumption.step_size ?? 1}
|
||||
value={value}
|
||||
onChange={(e) => 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<Set<string>>(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 (
|
||||
<div className="space-y-5 p-5 bg-white/[0.05] rounded-2xl border border-white/10">
|
||||
<h4 className="text-sm font-medium text-white/60">{i.financials.adjustAssumptions}</h4>
|
||||
<Slider
|
||||
label={i.financials.sliderGrowth}
|
||||
value={growthRate}
|
||||
min={50}
|
||||
max={200}
|
||||
step={10}
|
||||
unit="%"
|
||||
onChange={onGrowthChange}
|
||||
/>
|
||||
<Slider
|
||||
label={i.financials.sliderChurn}
|
||||
value={churnRate}
|
||||
min={0}
|
||||
max={15}
|
||||
step={0.5}
|
||||
unit="%"
|
||||
onChange={onChurnChange}
|
||||
/>
|
||||
<Slider
|
||||
label={i.financials.sliderArpu}
|
||||
value={arpu}
|
||||
min={200}
|
||||
max={1500}
|
||||
step={50}
|
||||
unit=" EUR"
|
||||
onChange={onArpuChange}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
{categories.filter(c => c.items.length > 0).map((cat) => {
|
||||
const isOpen = openCategories.has(cat.key)
|
||||
return (
|
||||
<div key={cat.key} className="border border-white/[0.06] rounded-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleCategory(cat.key)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-xs text-white/60 hover:text-white/80 transition-colors"
|
||||
>
|
||||
<span className="font-medium">{cat.label}</span>
|
||||
{isOpen ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="px-3 pb-3 space-y-3">
|
||||
{cat.items.map((a) => (
|
||||
<Slider
|
||||
key={a.key}
|
||||
assumption={a}
|
||||
onChange={(val) => onAssumptionChange(a.key, val)}
|
||||
lang={lang}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
59
pitch-deck/components/ui/KPICard.tsx
Normal file
59
pitch-deck/components/ui/KPICard.tsx
Normal file
@@ -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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay }}
|
||||
className="relative overflow-hidden bg-white/[0.06] backdrop-blur-xl border border-white/10 rounded-2xl p-4"
|
||||
>
|
||||
{/* Glow effect */}
|
||||
<div
|
||||
className="absolute -top-8 -right-8 w-24 h-24 rounded-full blur-3xl opacity-20"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
|
||||
<p className="text-[10px] uppercase tracking-wider text-white/40 mb-1">{label}</p>
|
||||
<div className="flex items-end gap-2">
|
||||
<p className="text-2xl font-bold text-white leading-none">
|
||||
<AnimatedCounter target={value} prefix={prefix} suffix={suffix} duration={1200} decimals={decimals} />
|
||||
</p>
|
||||
{trend !== 'neutral' && (
|
||||
<span className={`flex items-center gap-0.5 text-xs pb-0.5 ${trend === 'up' ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{trend === 'up' ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{subLabel && (
|
||||
<p className="text-[10px] text-white/30 mt-1">{subLabel}</p>
|
||||
)}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
133
pitch-deck/components/ui/RunwayGauge.tsx
Normal file
133
pitch-deck/components/ui/RunwayGauge.tsx
Normal file
@@ -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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className={`relative ${shouldPulse ? 'animate-pulse' : ''}`} style={{ width: size, height: size * 0.8 }}>
|
||||
<svg width={size} height={size * 0.8} viewBox={`0 0 ${size} ${size * 0.8}`}>
|
||||
{/* Background arc */}
|
||||
<path d={arcPath} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="8" strokeLinecap="round" />
|
||||
|
||||
{/* Filled arc */}
|
||||
<motion.path
|
||||
d={filledPath}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
transition={{ duration: 1.5, ease: 'easeOut' }}
|
||||
/>
|
||||
|
||||
{/* 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 (
|
||||
<g key={tick}>
|
||||
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke="rgba(255,255,255,0.3)" strokeWidth="1.5" />
|
||||
<text
|
||||
x={polarToCartesian(cx, cy, radius - 22, tickAngle).x}
|
||||
y={polarToCartesian(cx, cy, radius - 22, tickAngle).y}
|
||||
fill="rgba(255,255,255,0.3)"
|
||||
fontSize="8"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
>
|
||||
{tick}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Needle */}
|
||||
<motion.line
|
||||
x1={cx}
|
||||
y1={cy}
|
||||
x2={needleX}
|
||||
y2={needleY}
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
/>
|
||||
|
||||
{/* Center circle */}
|
||||
<circle cx={cx} cy={cy} r="4" fill={color} />
|
||||
<circle cx={cx} cy={cy} r="2" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="text-center -mt-2">
|
||||
<p className="text-lg font-bold" style={{ color }}>{Math.round(months)}</p>
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider">{label}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
63
pitch-deck/components/ui/ScenarioSwitcher.tsx
Normal file
63
pitch-deck/components/ui/ScenarioSwitcher.tsx
Normal file
@@ -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 (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] text-white/40 uppercase tracking-wider">
|
||||
{lang === 'de' ? 'Szenarien' : 'Scenarios'}
|
||||
</p>
|
||||
<button
|
||||
onClick={onToggleCompare}
|
||||
className={`text-[10px] px-2 py-1 rounded-lg transition-colors
|
||||
${compareMode
|
||||
? 'bg-indigo-500/30 text-indigo-300 border border-indigo-500/40'
|
||||
: 'bg-white/[0.06] text-white/40 border border-white/10 hover:text-white/60'
|
||||
}`}
|
||||
>
|
||||
{lang === 'de' ? 'Vergleichen' : 'Compare'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{scenarios.map((s) => (
|
||||
<motion.button
|
||||
key={s.id}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: s.color }}
|
||||
/>
|
||||
{s.name}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
pitch-deck/components/ui/UnitEconomicsCards.tsx
Normal file
93
pitch-deck/components/ui/UnitEconomicsCards.tsx
Normal file
@@ -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 (
|
||||
<svg width={size} height={size} className="shrink-0">
|
||||
<circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="3" />
|
||||
<motion.circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
initial={{ strokeDashoffset: circumference }}
|
||||
animate={{ strokeDashoffset: offset }}
|
||||
transition={{ duration: 1.5, ease: 'easeOut' }}
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{cards.map((card, i) => (
|
||||
<motion.div
|
||||
key={card.label}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 + i * 0.1 }}
|
||||
className="bg-white/[0.06] backdrop-blur-xl border border-white/10 rounded-xl p-3 text-center"
|
||||
>
|
||||
<div className="flex justify-center mb-2">
|
||||
<MiniRing progress={card.ring} color={card.color} />
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white">
|
||||
<AnimatedCounter target={card.value} suffix={card.suffix} duration={1000} />
|
||||
</p>
|
||||
<p className="text-[10px] text-white/40 mt-0.5">{card.label}</p>
|
||||
<p className="text-[9px] text-white/25 mt-0.5">{card.sub}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
85
pitch-deck/components/ui/WaterfallChart.tsx
Normal file
85
pitch-deck/components/ui/WaterfallChart.tsx
Normal file
@@ -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 (
|
||||
<div className="w-full h-[220px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={quarterlyData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
stroke="rgba(255,255,255,0.3)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 9 }}
|
||||
interval={1}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="rgba(255,255,255,0.1)"
|
||||
tick={{ fill: 'rgba(255,255,255,0.4)', fontSize: 10 }}
|
||||
tickFormatter={formatValue}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: 'rgba(10, 10, 26, 0.95)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
color: '#fff',
|
||||
fontSize: 11,
|
||||
}}
|
||||
formatter={(value: number, name: string) => [
|
||||
formatValue(value) + ' EUR',
|
||||
name === 'revenue' ? (lang === 'de' ? 'Umsatz' : 'Revenue')
|
||||
: name === 'costs' ? (lang === 'de' ? 'Kosten' : 'Costs')
|
||||
: 'Net',
|
||||
]}
|
||||
/>
|
||||
<ReferenceLine y={0} stroke="rgba(255,255,255,0.2)" />
|
||||
<Bar dataKey="revenue" radius={[3, 3, 0, 0]} barSize={14}>
|
||||
{quarterlyData.map((entry, i) => (
|
||||
<Cell key={i} fill="rgba(34, 197, 94, 0.7)" />
|
||||
))}
|
||||
</Bar>
|
||||
<Bar dataKey="costs" radius={[0, 0, 3, 3]} barSize={14}>
|
||||
{quarterlyData.map((entry, i) => (
|
||||
<Cell key={i} fill="rgba(239, 68, 68, 0.5)" />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
pitch-deck/lib/hooks/useFinancialModel.ts
Normal file
109
pitch-deck/lib/hooks/useFinancialModel.ts
Normal file
@@ -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<FMScenario[]>([])
|
||||
const [activeScenarioId, setActiveScenarioId] = useState<string | null>(null)
|
||||
const [compareMode, setCompareMode] = useState(false)
|
||||
const [results, setResults] = useState<Map<string, FMComputeResponse>>(new Map())
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [computing, setComputing] = useState(false)
|
||||
const computeTimer = useRef<NodeJS.Timeout | null>(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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user