Files
breakpilot-core/pitch-deck/components/slides/ArchitectureSlide.tsx
Sharang Parnerkar ac8ef371ff
All checks were successful
Build pitch-deck / build-push-deploy (push) Successful in 1m12s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 33s
CI / test-python-voice (push) Successful in 31s
CI / test-bqas (push) Successful in 30s
fix(pitch-deck): center mandants strip and fix LiteLLM overlap
- Remove flex-1 trailing divider that was pushing pills left
- Increase map height 420→480px to give clearance between app labels and gateway circle
- Update SVG viewBox to 0 0 1100 480 (consistent with CONNS coordinates)
- Update cy% for all nodes to match new 480px coordinate space (app 22.1%, gateway 50%, inference 80%)
- Update SEP1/SEP2 to 36%/65% for new height
- Update all SVG element y-coords: track lines, tick marks, junction dots, gateway stub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 23:48:33 +02:00

682 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { Language } from '@/lib/types'
import { t } from '@/lib/i18n'
import GradientText from '../ui/GradientText'
import FadeInView from '../ui/FadeInView'
import {
Brain, Shield, ScanLine, Zap, Cpu,
Layers, Wrench, X, Users, Lock,
Server, Network, ChevronRight, BadgeCheck,
} from 'lucide-react'
interface ArchitectureSlideProps { lang: Language }
type NodeId = 'certifai' | 'complai' | 'scanner' | 'litellm' | 'llm' | 'embeddings' | 'tools'
type ConnType = 'api' | 'mcp' | 'embed' | 'tool'
interface NodeDef {
id: NodeId
icon: React.ElementType
title: string
subtitle: string
color: string
twColor: string
twBorder: string
twBg: string
twDot: string
cx: number // % of container width (aligns with SVG x/1100)
cy: number // % of container height (aligns with SVG y/420)
tier: 'product' | 'proxy' | 'inference'
tech: string[]
services: { name: string; desc: string }[]
badge?: string
}
interface ConnDef {
from: NodeId
to: NodeId
type: ConnType
d: string // SVG path in viewBox "0 0 1100 480" — right-angle only
revD?: string
}
// All right-angle paths — safe with preserveAspectRatio="none" + vectorEffect="non-scaling-stroke"
// viewBox: 0 0 1100 480 | app y=106 gateway y=240 inference y=384
const CONNS: ConnDef[] = [
{ from: 'certifai', to: 'litellm', type: 'api',
d: 'M 195 106 L 195 178 L 522 178 L 522 240' },
{ from: 'complai', to: 'litellm', type: 'api',
d: 'M 550 106 L 550 240' },
{ from: 'scanner', to: 'litellm', type: 'api',
d: 'M 905 106 L 905 178 L 578 178 L 578 240' },
{ from: 'complai', to: 'scanner', type: 'mcp',
d: 'M 550 106 Q 727 55 905 106',
revD: 'M 905 106 Q 727 55 550 106' },
{ from: 'litellm', to: 'llm', type: 'api',
d: 'M 550 240 L 550 308 L 218 308 L 218 384' },
{ from: 'litellm', to: 'embeddings', type: 'embed',
d: 'M 550 240 L 550 384' },
{ from: 'litellm', to: 'tools', type: 'tool',
d: 'M 550 240 L 550 308 L 882 308 L 882 384' },
]
// cx = svgX/1100*100, cy = svgY/480*100 (exact match between SVG coords and CSS %)
function getNodes(de: boolean): NodeDef[] {
return [
{
id: 'certifai', icon: Brain,
title: 'CERTifAI',
subtitle: de ? 'GenAI Mandantenportal' : 'GenAI Tenant Portal',
color: '#c084fc', twColor: 'text-purple-400',
twBorder: 'border-purple-500/50', twBg: 'bg-purple-500/10', twDot: 'bg-purple-400',
cx: 17.7, cy: 22.1, tier: 'product',
badge: 'Rust · Dioxus',
tech: ['Rust', 'Dioxus', 'MongoDB', 'Keycloak', 'SearXNG', 'LangGraph'],
services: [
{ name: 'LiteLLM Dashboard', desc: de ? 'Modellverwaltung & Kostentracking' : 'Model mgmt & cost tracking' },
{ name: 'LibreChat + SSO', desc: de ? 'Mandanten-Chat mit Keycloak' : 'Tenant chat with Keycloak' },
{ name: 'LangGraph Agents', desc: de ? 'Agent-Orchestrierung' : 'Agent orchestration' },
{ name: 'MCP Hub', desc: de ? 'Tool-Integration für KI-Clients' : 'Tool integration for AI clients' },
],
},
{
id: 'complai', icon: Shield,
title: 'COMPLAI',
subtitle: de ? 'Compliance & Audit' : 'Compliance & Audit',
color: '#818cf8', twColor: 'text-indigo-400',
twBorder: 'border-indigo-500/50', twBg: 'bg-indigo-500/10', twDot: 'bg-indigo-400',
cx: 50, cy: 22.1, tier: 'product',
badge: 'Next.js · FastAPI',
tech: ['Next.js 15', 'FastAPI', 'Go/Gin', 'PostgreSQL', 'Qdrant', 'Valkey'],
services: [
{ name: de ? 'DSGVO / AI Act / NIS2' : 'GDPR / AI Act / NIS2', desc: de ? '70k+ auditierbare Controls' : '70k+ auditable controls' },
{ name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen, semantische Suche' : '75+ legal sources, semantic search' },
{ name: 'Control Pipeline', desc: de ? 'Gesetzestextanalyse via LLM' : 'Legal text analysis via LLM' },
{ name: 'MCP Client', desc: de ? 'Echtzeit-Findings vom Scanner' : 'Real-time findings from Scanner' },
],
},
{
id: 'scanner', icon: ScanLine,
title: 'Compliance Scanner',
subtitle: de ? 'Code-Sicherheit' : 'Code Security',
color: '#34d399', twColor: 'text-emerald-400',
twBorder: 'border-emerald-500/50', twBg: 'bg-emerald-500/10', twDot: 'bg-emerald-400',
cx: 82.3, cy: 22.1, tier: 'product',
badge: 'Rust · Axum',
tech: ['Rust', 'Axum', 'MongoDB', 'Semgrep', 'Gitleaks', 'Syft'],
services: [
{ name: 'SAST / SBOM / CVE', desc: de ? 'Vollautomatische Pipeline' : 'Fully automated pipeline' },
{ name: de ? 'KI-Triage' : 'AI Triage', desc: de ? 'LLM filtert False Positives' : 'LLM filters false positives' },
{ name: de ? 'KI-Pentest' : 'AI Pentest', desc: de ? 'Autonome Angriffsketten' : 'Autonomous attack chains' },
{ name: 'MCP Server', desc: de ? 'Live-Findings für COMPLAI' : 'Live findings for COMPLAI' },
],
},
{
id: 'litellm', icon: Zap,
title: 'LiteLLM Proxy',
subtitle: de ? 'KI-Gateway & Guardrails' : 'AI Gateway & Guardrails',
color: '#fbbf24', twColor: 'text-amber-400',
twBorder: 'border-amber-500/60', twBg: 'bg-amber-500/10', twDot: 'bg-amber-400',
cx: 50, cy: 50, tier: 'proxy',
badge: 'Hub',
tech: ['OpenAI-kompatible API', 'Bearer Auth', 'Rate Limiting', 'PII-Filter', 'Spend Tracking'],
services: [
{ name: de ? 'Token-Budget' : 'Token Budget', desc: de ? 'Pro-Mandant Kontingente & Abrechnung' : 'Per-tenant quotas & billing' },
{ name: 'PII Guardrails', desc: de ? 'Datenschutz-Filter für alle Anfragen' : 'Privacy filter on all requests' },
{ name: de ? 'Web-Suche (anonym)' : 'Web Search (anon)', desc: de ? 'SearXNG-Proxy, kein US-Anbieter' : 'SearXNG proxy, no US providers' },
{ name: de ? 'Namespace-Isolierung' : 'Namespace Isolation', desc: de ? 'Mandantentrennung per API-Key' : 'Tenant isolation per API key' },
{ name: de ? 'Failover-Routing' : 'Failover Routing', desc: de ? 'Automatisches Fallback' : 'Automatic fallback between models' },
],
},
{
id: 'llm', icon: Cpu,
title: de ? 'LLM Inferenz' : 'LLM Inference',
subtitle: de ? 'Lokale Sprachmodelle' : 'Local Language Models',
color: '#60a5fa', twColor: 'text-blue-400',
twBorder: 'border-blue-500/50', twBg: 'bg-blue-500/10', twDot: 'bg-blue-400',
cx: 17.7, cy: 80, tier: 'inference',
badge: 'On-Premise · BSI',
tech: ['Qwen3-32B', 'Qwen3-Coder-30B', 'DeepSeek-R1-8B', 'Ollama'],
services: [
{ name: de ? 'Vollständig lokal' : 'Fully local', desc: de ? 'Daten verlassen nie den Server' : 'Data never leaves the server' },
{ name: de ? 'Air-Gap fähig' : 'Air-Gap Capable', desc: de ? 'Kein Internet erforderlich' : 'No internet required' },
{ name: de ? 'GPU-optimiert' : 'GPU-optimized', desc: de ? 'Dedizierte Inferenz-Hardware' : 'Dedicated inference hardware' },
],
},
{
id: 'embeddings', icon: Layers,
title: 'Embeddings',
subtitle: de ? 'Semantische Suche' : 'Semantic Search',
color: '#a78bfa', twColor: 'text-violet-400',
twBorder: 'border-violet-500/50', twBg: 'bg-violet-500/10', twDot: 'bg-violet-400',
cx: 50, cy: 80, tier: 'inference',
badge: de ? 'EU-Souverän' : 'EU Sovereign',
tech: ['bge-m3', 'Qdrant Vector DB', 'Sentence-Transformers'],
services: [
{ name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen indexiert' : '75+ legal sources indexed' },
{ name: de ? 'Semantische Suche' : 'Semantic Search', desc: de ? 'Multi-linguale Einbettungen' : 'Multi-lingual embeddings' },
{ name: de ? 'Lokal' : 'Fully local', desc: de ? 'Keine externen APIs' : 'No external APIs' },
],
},
{
id: 'tools', icon: Wrench,
title: de ? 'KI-Tools' : 'AI Tools',
subtitle: de ? 'Web-Suche & MCP' : 'Web Search & MCP',
color: '#2dd4bf', twColor: 'text-teal-400',
twBorder: 'border-teal-500/50', twBg: 'bg-teal-500/10', twDot: 'bg-teal-400',
cx: 82.3, cy: 80, tier: 'inference',
badge: de ? 'EU-Souverän' : 'EU Sovereign',
tech: ['SearXNG', 'MCP Protocol', 'Semgrep API', 'Gitleaks API'],
services: [
{ name: 'SearXNG', desc: de ? 'Anonymisierte EU-Websuche' : 'Anonymized EU web search' },
{ name: 'MCP Tools', desc: de ? 'Auditdokumente & Code-Findings' : 'Audit docs & code findings' },
{ name: de ? 'Kein US-Anbieter' : 'No US providers', desc: de ? '100% DSGVO-konform' : '100% GDPR-compliant' },
],
},
]
}
// ── Token counter ─────────────────────────────────────────────────────────────
function TokenTicker({ color }: { color: string }) {
const [n, setN] = useState(() => 12480 + Math.floor(Math.random() * 8000))
useEffect(() => {
const id = setInterval(() => setN(v => v + Math.floor(Math.random() * 180 + 40)), 220)
return () => clearInterval(id)
}, [])
return (
<div className="flex items-center gap-1">
<motion.div className="w-1 h-1 rounded-full" style={{ background: color }}
animate={{ opacity: [1, 0.3, 1] }} transition={{ duration: 0.6, repeat: Infinity }} />
<span className="text-[7px] font-mono tabular-nums" style={{ color, opacity: 0.65 }}>
{n.toLocaleString('de-DE')} tok/s
</span>
</div>
)
}
// ── Data-flow packets along metro tracks ──────────────────────────────────────
function DataFlow({ d, revD, color, active, type }: {
d: string; revD?: string; color: string; active: boolean; type: ConnType
}) {
const isMcp = type === 'mcp'
const dashLen = active ? 18 : 8
const gapLen = active ? 110 : 280
const speed = active ? 1.9 : 5.5
const cycle = dashLen + gapLen
const op = active ? 0.9 : 0.16
const pw = active ? 3.5 : 1.5
return (
<g>
{/* Track rail */}
<path d={d} fill="none" stroke={color}
strokeWidth={active ? 1.8 : 0.9}
strokeOpacity={active ? 0.38 : 0.08}
strokeDasharray={isMcp ? '6 12' : undefined}
vectorEffect="non-scaling-stroke"
/>
{/* Primary packet */}
<motion.path d={d} fill="none" stroke={color}
strokeWidth={pw} strokeLinecap="round"
strokeDasharray={`${dashLen} ${gapLen}`}
strokeOpacity={op}
vectorEffect="non-scaling-stroke"
initial={{ strokeDashoffset: 0 }}
animate={{ strokeDashoffset: -cycle }}
transition={{ duration: speed, repeat: Infinity, ease: 'linear' }}
/>
{/* Staggered trailing packet when active */}
{active && (
<motion.path d={d} fill="none" stroke={color}
strokeWidth={pw * 0.65} strokeLinecap="round"
strokeDasharray={`${Math.round(dashLen * 0.7)} ${gapLen}`}
strokeOpacity={op * 0.55}
vectorEffect="non-scaling-stroke"
initial={{ strokeDashoffset: Math.round(cycle * 0.55) }}
animate={{ strokeDashoffset: Math.round(cycle * 0.55) - cycle }}
transition={{ duration: speed, repeat: Infinity, ease: 'linear' }}
/>
)}
{/* MCP reverse flow */}
{isMcp && revD && (
<motion.path d={revD} fill="none" stroke={color}
strokeWidth={active ? 2.5 : 1.2} strokeLinecap="round"
strokeDasharray={`${Math.round(dashLen * 0.7)} ${gapLen}`}
strokeOpacity={op * 0.72}
vectorEffect="non-scaling-stroke"
initial={{ strokeDashoffset: Math.round(cycle * 0.48) }}
animate={{ strokeDashoffset: Math.round(cycle * 0.48) - cycle }}
transition={{ duration: speed * 1.25, repeat: Infinity, ease: 'linear' }}
/>
)}
</g>
)
}
// ── Metro station card ────────────────────────────────────────────────────────
// Outer div handles absolute positioning — never scaled.
// Inner motion.button handles hover scale only.
function MetroStation({ node, active, onClick, de }: {
node: NodeDef; active: boolean; onClick: () => void; de: boolean
}) {
const Icon = node.icon
const isHub = node.id === 'litellm'
const sz = isHub ? 82 : 52
return (
<div className="absolute" style={{
left: `${node.cx}%`, top: `${node.cy}%`, transform: 'translate(-50%, -50%)',
}}>
<motion.button
onClick={onClick}
whileHover={{ scale: 1.06 }}
whileTap={{ scale: 0.96 }}
transition={{ type: 'spring', stiffness: 380, damping: 22 }}
className="focus:outline-none flex flex-col items-center"
style={{ gap: 6 }}
>
{/* Station circle */}
<div className="relative flex items-center justify-center" style={{ width: sz, height: sz }}>
{/* Outer pulse ring on active */}
{active && (
<motion.div className="absolute rounded-full pointer-events-none"
style={{ inset: -10, border: `1.5px solid ${node.color}`, borderRadius: '50%' }}
animate={{ scale: [1, 1.55], opacity: [0.45, 0] }}
transition={{ duration: 1.9, repeat: Infinity, ease: 'easeOut' }}
/>
)}
{/* Hub ambient glow */}
{isHub && (
<div className="absolute rounded-full pointer-events-none"
style={{ inset: -6, background: `radial-gradient(circle, ${node.color}22, transparent)`, borderRadius: '50%' }}
/>
)}
{/* Main circle */}
<div className="rounded-full flex items-center justify-center"
style={{
width: sz, height: sz,
background: active
? `radial-gradient(circle at 38% 32%, ${node.color}38, ${node.color}14)`
: 'rgba(6, 12, 28, 0.92)',
border: `${active ? 2 : 1.5}px solid ${active ? node.color : node.color + '52'}`,
boxShadow: active
? `0 0 26px -4px ${node.color}75, inset 0 0 18px ${node.color}18`
: 'none',
transition: 'all 0.22s ease',
}}
>
<Icon style={{
width: isHub ? 26 : 20,
height: isHub ? 26 : 20,
color: active ? node.color : `${node.color}78`,
transition: 'color 0.2s ease',
}} />
</div>
{/* Hub spinning dashed ring */}
{isHub && active && (
<motion.div className="absolute pointer-events-none"
style={{ inset: -11, borderRadius: '50%', border: `1px dashed ${node.color}48` }}
animate={{ rotate: 360 }}
transition={{ duration: 12, repeat: Infinity, ease: 'linear' }}
/>
)}
{/* Active indicator dot */}
{active && (
<div className="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 rounded-full animate-pulse"
style={{ background: node.color, boxShadow: `0 0 7px ${node.color}` }}
/>
)}
</div>
{/* Station label */}
<div className="text-center pointer-events-none" style={{ maxWidth: isHub ? 136 : 114 }}>
<div
className={`font-semibold leading-tight ${isHub ? 'text-[11px]' : 'text-[9.5px]'}`}
style={{ color: active ? node.color : 'rgba(255,255,255,0.72)', transition: 'color 0.2s ease' }}
>
{node.title}
</div>
<div className="text-[8px] leading-tight mt-0.5"
style={{ color: active ? `${node.color}c0` : 'rgba(255,255,255,0.28)', transition: 'color 0.2s ease' }}>
{node.subtitle}
</div>
{node.badge && (
<div className="mt-1 inline-block text-[7px] px-1.5 py-[1px] rounded font-mono"
style={{
border: `1px solid ${active ? node.color + '65' : 'rgba(255,255,255,0.1)'}`,
color: active ? node.color : 'rgba(255,255,255,0.26)',
background: active ? `${node.color}14` : 'transparent',
transition: 'all 0.2s ease',
}}>
{node.badge}
</div>
)}
{active && node.tier === 'inference' && (
<div className="mt-1.5"><TokenTicker color={node.color} /></div>
)}
</div>
</motion.button>
</div>
)
}
// ── Main slide ────────────────────────────────────────────────────────────────
export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
const i = t(lang)
const de = lang === 'de'
const nodes = getNodes(de)
const [activeId, setActiveId] = useState<NodeId | null>(null)
const active = nodes.find(n => n.id === activeId) ?? null
function toggle(id: NodeId) { setActiveId(prev => prev === id ? null : id) }
const tenants = de
? ['Mandant A', 'Mandant B', 'Mandant C', 'Mandant N…']
: ['Namespace A', 'Namespace B', 'Namespace C', 'Namespace N…']
// Tier separator positions (% of 480px container height)
// App: 036% (y=0173), Gateway: 3665% (y=173312), Inference: 65100%
const SEP1 = '36%'
const SEP2 = '65%'
return (
<div className="space-y-3">
{/* Header */}
<FadeInView className="text-center mb-3">
<p className="text-[10px] font-mono text-indigo-400/50 uppercase tracking-widest mb-1.5">
{de ? 'Anhang' : 'Appendix'}
</p>
<h2 className="text-3xl md:text-4xl font-bold mb-1.5">
<GradientText>{i.annex.architecture.title}</GradientText>
</h2>
<p className="text-xs text-white/35">
{de ? 'Klicke auf eine Station für Details' : 'Click any station to explore'}
</p>
</FadeInView>
<FadeInView delay={0.15} className="space-y-3">
{/* Customer namespace strip */}
<div className="flex items-center justify-center gap-2 flex-wrap px-[4%]">
<Users className="w-3 h-3 text-white/25 flex-shrink-0" />
<span className="text-[9px] font-mono text-white/25 uppercase tracking-widest mr-1">
{de ? 'Kundenmandanten' : 'Customer Namespaces'}
</span>
{tenants.map(tn => (
<span key={tn}
className="text-[9px] px-2 py-0.5 rounded-full border border-white/[0.08] bg-white/[0.03] text-white/35 font-mono">
{tn}
</span>
))}
</div>
{/* ── METRO MAP ──────────────────────────────────────────────── */}
<div className="relative w-full rounded-xl overflow-hidden" style={{ height: '480px' }}>
{/* Background */}
<div className="absolute inset-0"
style={{ background: 'linear-gradient(175deg, #030c1e 0%, #040e24 55%, #030c1c 100%)' }}>
<div className="absolute inset-0"
style={{
backgroundImage: 'radial-gradient(circle, rgba(100,110,140,0.18) 1px, transparent 1px)',
backgroundSize: '28px 28px',
opacity: 0.4,
}}
/>
</div>
{/* Tier band fills */}
<div className="absolute inset-x-0 top-0 pointer-events-none"
style={{ height: SEP1, background: 'rgba(129,140,248,0.028)' }} />
<div className="absolute inset-x-0 pointer-events-none"
style={{ top: SEP1, height: '29%', background: 'rgba(251,191,36,0.032)' }} />
<div className="absolute inset-x-0 pointer-events-none"
style={{ top: SEP2, bottom: 0, background: 'rgba(96,165,250,0.022)' }} />
{/* Tier separator lines */}
{[SEP1, SEP2].map(y => (
<div key={y} className="absolute inset-x-0 h-px pointer-events-none"
style={{ top: y, background: 'linear-gradient(to right, transparent 2%, rgba(255,255,255,0.07) 15%, rgba(255,255,255,0.07) 85%, transparent 98%)' }}
/>
))}
{/* Vertical tier labels — left strip */}
<div className="absolute left-0 top-0 bottom-0 w-7 flex flex-col pointer-events-none select-none" style={{ zIndex: 2 }}>
<div className="flex items-center justify-center" style={{ height: SEP1 }}>
<span className="text-[6.5px] font-mono tracking-[0.22em] uppercase"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'rgba(129,140,248,0.38)' }}>
{de ? 'ANWENDUNG' : 'APP LAYER'}
</span>
</div>
<div className="flex items-center justify-center" style={{ height: '29%' }}>
<span className="text-[6.5px] font-mono tracking-[0.22em] uppercase"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'rgba(251,191,36,0.38)' }}>
GATEWAY
</span>
</div>
<div className="flex items-center justify-center flex-1">
<span className="text-[6.5px] font-mono tracking-[0.22em] uppercase"
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)', color: 'rgba(96,165,250,0.38)' }}>
{de ? 'INFERENZ' : 'INFERENCE'}
</span>
</div>
</div>
{/* BSI badges — right strip */}
<div className="absolute right-0 top-0 bottom-0 w-20 flex flex-col pointer-events-none select-none" style={{ zIndex: 2 }}>
<div style={{ height: SEP1 }} />
<div className="flex items-center justify-end pr-2" style={{ height: '29%' }}>
<div className="flex items-center gap-1 opacity-45">
<BadgeCheck className="w-2.5 h-2.5 text-amber-400" />
<span className="text-[6.5px] font-mono tracking-wider text-amber-400">BSI DC</span>
</div>
</div>
<div className="flex items-center justify-end pr-2 flex-1">
<div className="flex items-center gap-1 opacity-45">
<BadgeCheck className="w-2.5 h-2.5 text-blue-400" />
<span className="text-[6.5px] font-mono tracking-wider text-blue-400">
{de ? 'EU-Souverän' : 'EU Sovereign'}
</span>
</div>
</div>
</div>
{/* MCP label */}
<div className="absolute pointer-events-none select-none"
style={{ left: '66%', top: '8%', transform: 'translate(-50%, -50%)', zIndex: 3 }}>
<span className="text-[7px] px-2 py-[2px] rounded-full border border-emerald-500/35 text-emerald-400/65 font-mono tracking-wider">
MCP
</span>
</div>
{/* ── SVG: metro tracks + animated data flows ── */}
<svg className="absolute inset-0 w-full h-full pointer-events-none"
viewBox="0 0 1100 480" preserveAspectRatio="none">
{/* Horizontal metro line — Application layer (y=106) */}
<line x1="165" y1="106" x2="935" y2="106"
stroke="rgba(129,140,248,0.14)" strokeWidth="4"
vectorEffect="non-scaling-stroke" />
{/* Station tick marks on app line */}
{[195, 550, 905].map(x => (
<line key={x} x1={x} y1="98" x2={x} y2="114"
stroke="rgba(129,140,248,0.45)" strokeWidth="2"
vectorEffect="non-scaling-stroke" />
))}
{/* Horizontal metro line — Inference layer (y=384) */}
<line x1="165" y1="384" x2="935" y2="384"
stroke="rgba(96,165,250,0.14)" strokeWidth="4"
vectorEffect="non-scaling-stroke" />
{/* Station tick marks on inference line */}
{[195, 550, 905].map(x => (
<line key={x} x1={x} y1="376" x2={x} y2="392"
stroke="rgba(96,165,250,0.45)" strokeWidth="2"
vectorEffect="non-scaling-stroke" />
))}
{/* Gateway stub lines (short horizontal stubs from hub) */}
<line x1="440" y1="240" x2="660" y2="240"
stroke="rgba(251,191,36,0.10)" strokeWidth="3"
vectorEffect="non-scaling-stroke" />
{/* Junction corner dots */}
{[
{ cx: 195, cy: 178 }, { cx: 522, cy: 178 },
{ cx: 905, cy: 178 }, { cx: 578, cy: 178 },
{ cx: 218, cy: 308 }, { cx: 550, cy: 308 }, { cx: 882, cy: 308 },
].map(({ cx, cy }) => (
<circle key={`${cx}-${cy}`} cx={cx} cy={cy} r="3.5"
fill="rgba(160,170,200,0.22)" vectorEffect="non-scaling-stroke" />
))}
{/* Connection flows */}
{CONNS.map(c => {
const fromNode = nodes.find(n => n.id === c.from)!
const toNode = nodes.find(n => n.id === c.to)!
const isActive = activeId === c.from || activeId === c.to
const color = isActive
? (activeId === c.from ? fromNode.color : toNode.color)
: '#ffffff'
return (
<DataFlow key={`${c.from}-${c.to}`}
d={c.d} revD={c.revD}
color={color} active={isActive} type={c.type}
/>
)
})}
</svg>
{/* Metro station nodes */}
{nodes.map(node => (
<MetroStation key={node.id} node={node}
active={activeId === node.id}
onClick={() => toggle(node.id)}
de={de}
/>
))}
{!activeId && (
<div className="absolute bottom-3 right-3 flex items-center gap-1 text-[8px] text-white/18 pointer-events-none select-none">
<ChevronRight className="w-3 h-3" />
{de ? 'Station anklicken' : 'Click any station'}
</div>
)}
</div>
{/* ── DETAIL PANEL ─────────────────────────────────────────────── */}
<AnimatePresence>
{active && (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.22, ease: [0.22, 1, 0.36, 1] }}
className={`relative rounded-2xl border ${active.twBorder} ${active.twBg} p-4`}
style={{ boxShadow: `0 0 32px -10px ${active.color}45` }}
>
<button onClick={() => setActiveId(null)}
className="absolute top-3 right-3 w-6 h-6 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center transition-colors">
<X className="w-3 h-3 text-white/50" />
</button>
<div className="flex items-start gap-3 mb-4">
<div className={`w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0 border ${active.twBorder} ${active.twBg}`}>
<active.icon className={`w-5 h-5 ${active.twColor}`} />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-bold text-white">{active.title}</h3>
{active.badge && (
<span className={`text-[9px] px-1.5 py-0.5 rounded uppercase font-semibold tracking-wider border ${active.twBorder} ${active.twColor}`}>
{active.badge}
</span>
)}
<span className="text-[8px] px-1.5 py-0.5 rounded border border-white/10 text-white/35">
{active.tier === 'product' ? (de ? 'Anwendungsschicht' : 'Application Layer') :
active.tier === 'proxy' ? (de ? 'GenAI-Infrastruktur' : 'GenAI Infra') :
(de ? 'Inferenzschicht' : 'Inference Layer')}
</span>
</div>
<p className="text-xs text-white/45 mt-0.5">{active.subtitle}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-5">
<div>
<p className="text-[9px] uppercase tracking-widest text-white/30 mb-2 font-semibold">
{de ? 'Stack' : 'Tech Stack'}
</p>
<div className="flex flex-wrap gap-1.5">
{active.tech.map(tk => (
<span key={tk} className="text-[10px] px-2 py-0.5 rounded bg-white/[0.05] border border-white/[0.08] text-white/55 font-mono">
{tk}
</span>
))}
</div>
</div>
<div>
<p className="text-[9px] uppercase tracking-widest text-white/30 mb-2 font-semibold">
{de ? 'Funktionen' : 'Capabilities'}
</p>
<div className="space-y-1.5">
{active.services.map(s => (
<div key={s.name} className="flex items-start gap-2">
<div className={`w-1 h-1 rounded-full mt-1.5 flex-shrink-0 ${active.twDot} opacity-70`} />
<div className="min-w-0">
<span className="text-xs font-semibold text-white/80">{s.name}</span>
<span className="text-[10px] text-white/40 ml-1.5">{s.desc}</span>
</div>
</div>
))}
</div>
</div>
</div>
<div className="mt-3 pt-3 border-t border-white/[0.06] flex items-center gap-2 flex-wrap">
<Network className="w-3 h-3 text-white/20 flex-shrink-0" />
<span className="text-[9px] text-white/30">
{de ? 'Verbunden mit:' : 'Connects to:'}
</span>
{CONNS
.filter(c => c.from === active.id || c.to === active.id)
.map(c => {
const peerId = c.from === active.id ? c.to : c.from
const peer = nodes.find(n => n.id === peerId)!
const label = c.type === 'mcp' ? 'MCP' : c.type === 'embed' ? 'Embed' : c.type === 'tool' ? 'Tool' : 'API'
return (
<button key={peerId} onClick={() => setActiveId(peerId)}
className={`text-[9px] px-2 py-0.5 rounded-full border ${peer.twBorder} ${peer.twColor} transition-opacity hover:opacity-80 flex items-center gap-1`}>
{peer.title}
<span className="opacity-50 text-[7px]">{label}</span>
</button>
)
})}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Legend */}
{!active && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}
className="flex items-center justify-center gap-6 flex-wrap">
{[
{ icon: Lock, text: de ? 'Kein US-Anbieter · 100% DSGVO' : 'No US providers · 100% GDPR' },
{ icon: Server, text: de ? 'BSI-zertifiziertes Rechenzentrum' : 'BSI-certified data center' },
{ icon: BadgeCheck, text: de ? 'EU-souveräne Inferenz' : 'EU-sovereign inference' },
].map(({ icon: Icon, text }) => (
<span key={text} className="flex items-center gap-1.5 text-[10px] text-white/22">
<Icon className="w-3 h-3" />{text}
</span>
))}
</motion.div>
)}
</FadeInView>
</div>
)
}