diff --git a/pitch-deck/components/slides/ArchitectureSlide.tsx b/pitch-deck/components/slides/ArchitectureSlide.tsx index a9c182d..6c972ec 100644 --- a/pitch-deck/components/slides/ArchitectureSlide.tsx +++ b/pitch-deck/components/slides/ArchitectureSlide.tsx @@ -12,9 +12,7 @@ import { Server, Network, ChevronRight, BadgeCheck, } from 'lucide-react' -interface ArchitectureSlideProps { - lang: Language -} +interface ArchitectureSlideProps { lang: Language } type NodeId = 'certifai' | 'complai' | 'scanner' | 'litellm' | 'llm' | 'embeddings' | 'tools' type ConnType = 'api' | 'mcp' | 'embed' | 'tool' @@ -29,8 +27,8 @@ interface NodeDef { twBorder: string twBg: string twDot: string - cx: number - cy: number + 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 }[] @@ -41,30 +39,39 @@ interface ConnDef { from: NodeId to: NodeId type: ConnType - d: string - revD?: string // reverse path for bidirectional MCP + d: string // SVG path in viewBox "0 0 1100 420" — right-angle only + revD?: string } +// All right-angle paths — safe with preserveAspectRatio="none" + vectorEffect="non-scaling-stroke" const CONNS: ConnDef[] = [ - { from: 'certifai', to: 'litellm', type: 'api', d: 'M 18 22 Q 28 43 50 52' }, - { from: 'complai', to: 'litellm', type: 'api', d: 'M 50 22 L 50 52' }, - { from: 'scanner', to: 'litellm', type: 'api', d: 'M 82 22 Q 72 43 50 52' }, - { from: 'complai', to: 'scanner', type: 'mcp', d: 'M 50 22 Q 66 9 82 22', revD: 'M 82 22 Q 66 9 50 22' }, - { from: 'litellm', to: 'llm', type: 'api', d: 'M 50 52 Q 37 67 18 82' }, - { from: 'litellm', to: 'embeddings', type: 'embed', d: 'M 50 52 L 50 82' }, - { from: 'litellm', to: 'tools', type: 'tool', d: 'M 50 52 Q 63 67 82 82' }, + { from: 'certifai', to: 'litellm', type: 'api', + d: 'M 195 105 L 195 158 L 522 158 L 522 210' }, + { from: 'complai', to: 'litellm', type: 'api', + d: 'M 550 105 L 550 210' }, + { from: 'scanner', to: 'litellm', type: 'api', + d: 'M 905 105 L 905 158 L 578 158 L 578 210' }, + { from: 'complai', to: 'scanner', type: 'mcp', + d: 'M 550 105 Q 727 52 905 105', + revD: 'M 905 105 Q 727 52 550 105' }, + { from: 'litellm', to: 'llm', type: 'api', + d: 'M 550 210 L 550 268 L 218 268 L 218 340' }, + { from: 'litellm', to: 'embeddings', type: 'embed', + d: 'M 550 210 L 550 340' }, + { from: 'litellm', to: 'tools', type: 'tool', + d: 'M 550 210 L 550 268 L 882 268 L 882 340' }, ] +// cx = svgX/1100*100, cy = svgY/420*100 (exact match between SVG coords and CSS %) function getNodes(de: boolean): NodeDef[] { return [ { - id: 'certifai', - icon: Brain, + 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: 18, cy: 22, tier: 'product', + cx: 17.7, cy: 25, tier: 'product', badge: 'Rust · Dioxus', tech: ['Rust', 'Dioxus', 'MongoDB', 'Keycloak', 'SearXNG', 'LangGraph'], services: [ @@ -75,13 +82,12 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'complai', - icon: Shield, + 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, tier: 'product', + cx: 50, cy: 25, tier: 'product', badge: 'Next.js · FastAPI', tech: ['Next.js 15', 'FastAPI', 'Go/Gin', 'PostgreSQL', 'Qdrant', 'Valkey'], services: [ @@ -92,13 +98,12 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'scanner', - icon: ScanLine, + 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, cy: 22, tier: 'product', + cx: 82.3, cy: 25, tier: 'product', badge: 'Rust · Axum', tech: ['Rust', 'Axum', 'MongoDB', 'Semgrep', 'Gitleaks', 'Syft'], services: [ @@ -109,13 +114,12 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'litellm', - icon: Zap, + 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: 52, tier: 'proxy', + cx: 50, cy: 50, tier: 'proxy', badge: 'Hub', tech: ['OpenAI-kompatible API', 'Bearer Auth', 'Rate Limiting', 'PII-Filter', 'Spend Tracking'], services: [ @@ -127,14 +131,13 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'llm', - icon: Cpu, + 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: 18, cy: 82, tier: 'inference', - badge: de ? 'On-Premise · BSI' : 'On-Premise · BSI', + cx: 17.7, cy: 81, 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' }, @@ -143,13 +146,12 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'embeddings', - icon: Layers, + 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: 82, tier: 'inference', + cx: 50, cy: 81, tier: 'inference', badge: de ? 'EU-Souverän' : 'EU Sovereign', tech: ['bge-m3', 'Qdrant Vector DB', 'Sentence-Transformers'], services: [ @@ -159,13 +161,12 @@ function getNodes(de: boolean): NodeDef[] { ], }, { - id: 'tools', - icon: Wrench, + 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, cy: 82, tier: 'inference', + cx: 82.3, cy: 81, tier: 'inference', badge: de ? 'EU-Souverän' : 'EU Sovereign', tech: ['SearXNG', 'MCP Protocol', 'Semgrep API', 'Gitleaks API'], services: [ @@ -177,7 +178,7 @@ function getNodes(de: boolean): NodeDef[] { ] } -// ── Token counter (inference tier ambient animation) ───────────────────────── +// ── Token counter ───────────────────────────────────────────────────────────── function TokenTicker({ color }: { color: string }) { const [n, setN] = useState(() => 12480 + Math.floor(Math.random() * 8000)) useEffect(() => { @@ -186,12 +187,8 @@ function TokenTicker({ color }: { color: string }) { }, []) return (
- + {n.toLocaleString('de-DE')} tok/s @@ -199,158 +196,173 @@ function TokenTicker({ color }: { color: string }) { ) } -// ── Data-flow path (always-on packets, fast when active) ───────────────────── -function DataFlow({ - d, revD, color, active, type, -}: { +// ── 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 speed = active ? 1.2 : 5 - const op = active ? 0.85 : 0.2 - const w = active ? 2.5 : 1.2 - const dash = active ? 3 : 2 + 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 ( - {/* Base line */} - - - {/* Forward packets */} - {[0, -28, -56].map((off, i) => ( - + {/* Staggered trailing packet when active */} + {active && ( + - ))} - - {/* Reverse packets for MCP (bidirectional) */} - {isMcp && revD && [0, -42].map((off, i) => ( - - ))} + )} ) } -// ── Node card ────────────────────────────────────────────────────────────────── -// Position wrapper (div) is never scaled — only the inner motion.button scales. -// This prevents the "snap" that happened when translate(-50%,-50%) and scale -// were on the same element. -function NodeCard({ - node, active, onClick, de, -}: { +// ── 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 Icon = node.icon + const isHub = node.id === 'litellm' + const sz = isHub ? 82 : 52 return ( -
+
-
- {/* Left accent bar */} -
- - {/* Icon + title */} -
-
- -
- - {node.title} - - {active && ( -
- )} + {/* Station circle */} +
+ {/* Outer pulse ring on active */} + {active && ( + + )} + {/* Hub ambient glow */} + {isHub && ( +
+ )} + {/* Main circle */} +
+
- - {/* Subtitle */} -

- {node.subtitle} -

- - {/* Badge */} - {node.badge && ( -
- - {node.badge} - -
- )} - - {/* Token ticker on inference nodes */} - {active && node.tier === 'inference' && ( -
- -
- )} - - {/* Hub: spinning indicator ring */} + {/* Hub spinning dashed ring */} {isHub && active && ( - )} + {/* Active indicator dot */} + {active && ( +
+ )} +
+ + {/* Station label */} +
+
+ {node.title} +
+
+ {node.subtitle} +
+ {node.badge && ( +
+ {node.badge} +
+ )} + {active && node.tier === 'inference' && ( +
+ )}
) } -// ── Main slide ───────────────────────────────────────────────────────────────── +// ── Main slide ──────────────────────────────────────────────────────────────── export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { const i = t(lang) const de = lang === 'de' @@ -359,19 +371,16 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { const [activeId, setActiveId] = useState(null) const active = nodes.find(n => n.id === activeId) ?? null - function toggle(id: NodeId) { - setActiveId(prev => (prev === id ? null : id)) - } + 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…'] - const tiers = [ - { y: '22%', label: de ? 'Anwendungsschicht' : 'Application Layer', clr: '#818cf8', badge: null }, - { y: '52%', label: de ? 'GenAI-Infrastruktur' : 'GenAI Infrastructure', clr: '#fbbf24', badge: de ? 'BSI-Rechenzentrum' : 'BSI Data Center' }, - { y: '82%', label: de ? 'Inferenzschicht' : 'Inference Layer', clr: '#60a5fa', badge: de ? 'BSI · EU-Souverän' : 'BSI · EU Sovereign' }, - ] + // Tier separator positions (% of 420px container height) + // App: 0–38.1% (y=0–160), Gateway: 38.1–64.3% (y=160–270), Inference: 64.3–100% + const SEP1 = '38.1%' + const SEP2 = '64.3%' return (
@@ -384,13 +393,13 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { {i.annex.architecture.title}

- {de ? 'Klicke auf eine Komponente für Details' : 'Click any component to explore'} + {de ? 'Klicke auf eine Station für Details' : 'Click any station to explore'}

{/* Customer namespace strip */} -
+
{de ? 'Kundenmandanten' : 'Customer Namespaces'} @@ -404,114 +413,136 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
- {/* ── MAP ──────────────────────────────────────────────────────────── */} -
+ {/* ── METRO MAP ──────────────────────────────────────────────── */} +
{/* Background */} -
-
+
+ {/* Tier band fills */} +
+
+
+ {/* Tier separator lines */} - {['37%', '66%'].map(y => ( -
( +
))} - {/* Tier labels (left) + BSI badges (right) */} - {tiers.map(({ y, label, clr, badge }) => ( -
-
-
- - {label} + {/* Vertical tier labels — left strip */} +
+
+ + {de ? 'ANWENDUNG' : 'APP LAYER'} + +
+
+ + GATEWAY + +
+
+ + {de ? 'INFERENZ' : 'INFERENCE'} + +
+
+ + {/* BSI badges — right strip */} +
+
+
+
+ + BSI DC +
+
+
+
+ + + {de ? 'EU-Souverän' : 'EU Sovereign'}
- {badge && ( -
- - - {badge} - -
- )}
- ))} - - {/* MCP badge between COMPLAI and Scanner */} -
- MCP ↔
- {/* Infra tech chips (secondary, very subtle, in the infra tier band) */} -
- {['PostgreSQL', 'MongoDB', 'Qdrant', 'Valkey', 'Nginx', 'Vault', 'Gitea', 'Orca', 'Woodpecker'].map(chip => ( - - {chip} - - ))} + {/* MCP label */} +
+ + ⇌ MCP +
- {/* SVG: island territories + data-flow paths */} + {/* ── SVG: metro tracks + animated data flows ── */} + viewBox="0 0 1100 420" preserveAspectRatio="none"> - {/* Tier zone fills */} - - - + {/* Horizontal metro line — Application layer (y=105) */} + + {/* Station tick marks on app line */} + {[195, 550, 905].map(x => ( + + ))} - {/* Island blobs per node */} - {nodes.map(n => { - const isHub = n.id === 'litellm' - const isActive = activeId === n.id - return ( - - - - ) - })} + {/* Horizontal metro line — Inference layer (y=340) */} + + {/* Station tick marks on inference line */} + {[195, 550, 905].map(x => ( + + ))} + + {/* Gateway stub lines (short horizontal stubs from hub) */} + + + {/* Junction corner dots */} + {[ + { cx: 195, cy: 158 }, { cx: 522, cy: 158 }, + { cx: 905, cy: 158 }, { cx: 578, cy: 158 }, + { cx: 218, cy: 268 }, { cx: 550, cy: 268 }, { cx: 882, cy: 268 }, + ].map(({ cx, cy }) => ( + + ))} {/* 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 + 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 ( - @@ -519,11 +550,9 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { })} - {/* Node cards */} + {/* Metro station nodes */} {nodes.map(node => ( - toggle(node.id)} de={de} @@ -531,14 +560,14 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { ))} {!activeId && ( -
+
- {de ? 'Komponente anklicken' : 'Click any node'} + {de ? 'Station anklicken' : 'Click any station'}
)}
- {/* ── DETAIL PANEL ──────────────────────────────────────────────────── */} + {/* ── DETAIL PANEL ─────────────────────────────────────────────── */} {active && ( - - {/* Header */}
@@ -569,7 +595,7 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { {active.badge} )} - + {active.tier === 'product' ? (de ? 'Anwendungsschicht' : 'Application Layer') : active.tier === 'proxy' ? (de ? 'GenAI-Infrastruktur' : 'GenAI Infra') : (de ? 'Inferenzschicht' : 'Inference Layer')} @@ -579,7 +605,6 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {
- {/* Body */}

@@ -611,7 +636,6 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) {

- {/* Connects-to */}
@@ -624,11 +648,8 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { const peer = nodes.find(n => n.id === peerId)! const label = c.type === 'mcp' ? 'MCP' : c.type === 'embed' ? 'Embed' : c.type === 'tool' ? 'Tool' : 'API' return ( - @@ -639,18 +660,17 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { )} - {/* Bottom legend */} + {/* Legend */} {!active && ( {[ - { 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: 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 }) => ( - - {text} + {text} ))}