From 34e2614e366ae6e3066a21a4c0fe22eb1a846009 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:06:45 +0200 Subject: [PATCH] feat(pitch-deck): redesign architecture slide with V4 layered stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Claude Design's V4 design exactly: three 3D perspective slabs (Application / Gateway / Infrastructure) with animated data-flow connectors between them, per-node live activity tickers, and a slide-up detail panel. Replaces metro map with V4's floating slab aesthetic — dark purple gradient background, CSS perspective rotateX transforms, JetBrains Mono terminal tickers, amber LiteLLM hub with pulse indicator. All node data (titles, tech stacks, services) preserved from previous design. Co-Authored-By: Claude Sonnet 4.6 --- .../components/slides/ArchitectureSlide.tsx | 1022 +++++++++-------- 1 file changed, 528 insertions(+), 494 deletions(-) diff --git a/pitch-deck/components/slides/ArchitectureSlide.tsx b/pitch-deck/components/slides/ArchitectureSlide.tsx index a14175a..f75b827 100644 --- a/pitch-deck/components/slides/ArchitectureSlide.tsx +++ b/pitch-deck/components/slides/ArchitectureSlide.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef, Fragment } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { Language } from '@/lib/types' import { t } from '@/lib/i18n' @@ -9,13 +9,11 @@ import FadeInView from '../ui/FadeInView' import { Brain, Shield, ScanLine, Zap, Cpu, Layers, Wrench, X, Users, Lock, - Server, Network, ChevronRight, BadgeCheck, + Server, 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 @@ -23,57 +21,19 @@ interface NodeDef { 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 + primary?: boolean + tier: 'product' | 'proxy' | 'inference' } -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', + color: '#c084fc', tier: 'product', tech: ['Rust', 'Dioxus', 'MongoDB', 'Keycloak', 'SearXNG', 'LangGraph'], services: [ { name: 'LiteLLM Dashboard', desc: de ? 'Modellverwaltung & Kostentracking' : 'Model mgmt & cost tracking' }, @@ -86,10 +46,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#818cf8', tier: 'product', 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' }, @@ -102,10 +59,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#34d399', tier: 'product', tech: ['Rust', 'Axum', 'MongoDB', 'Semgrep', 'Gitleaks', 'Syft'], services: [ { name: 'SAST / SBOM / CVE', desc: de ? 'Vollautomatische Pipeline' : 'Fully automated pipeline' }, @@ -118,10 +72,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#fbbf24', tier: 'proxy', primary: true, 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' }, @@ -135,10 +86,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#60a5fa', tier: 'inference', 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' }, @@ -150,10 +98,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#a78bfa', tier: 'inference', tech: ['bge-m3', 'Qdrant Vector DB', 'Sentence-Transformers'], services: [ { name: 'RAG Pipeline', desc: de ? '75+ Rechtsquellen indexiert' : '75+ legal sources indexed' }, @@ -165,10 +110,7 @@ function getNodes(de: boolean): NodeDef[] { 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', + color: '#2dd4bf', tier: 'inference', tech: ['SearXNG', 'MCP Protocol', 'Semgrep API', 'Gitleaks API'], services: [ { name: 'SearXNG', desc: de ? 'Anonymisierte EU-Websuche' : 'Anonymized EU web search' }, @@ -179,212 +121,389 @@ function getNodes(de: boolean): NodeDef[] { ] } -// ── Token counter ───────────────────────────────────────────────────────────── -function TokenTicker({ color }: { color: string }) { - const [n, setN] = useState(() => 12480 + Math.floor(Math.random() * 8000)) +const LAYERS: { id: string; nodeIds: NodeId[]; tint: string; depth: number }[] = [ + { id: 'product', nodeIds: ['certifai', 'complai', 'scanner'], tint: '#a78bfa', depth: 24 }, + { id: 'proxy', nodeIds: ['litellm'], tint: '#fbbf24', depth: 12 }, + { id: 'inference', nodeIds: ['llm', 'embeddings', 'tools'], tint: '#8b5cf6', depth: 0 }, +] + +const CSS_KF = ` + @keyframes v4FlowDown { from { stroke-dashoffset: 0 } to { stroke-dashoffset: -18px } } + @keyframes v4Pulse { 0%,100% { opacity:1;transform:scale(1) } 50% { opacity:.4;transform:scale(1.4) } } + @keyframes v4Caret { 0%,50% { opacity:1 } 51%,100% { opacity:0 } } + @keyframes v4DotFall { + 0% { transform: translateY(-5px); opacity: 0; } + 12% { opacity: 1; } + 88% { opacity: 1; } + 100% { transform: translateY(38px); opacity: 0; } + } +` + +// ── Ticker primitives ───────────────────────────────────────────────────────── +function useTicker(fn: () => void, min = 140, max = 360, skipChance = 0.1) { + const ref = useRef(fn) + ref.current = fn useEffect(() => { - const id = setInterval(() => setN(v => v + Math.floor(Math.random() * 180 + 40)), 220) - return () => clearInterval(id) - }, []) + let tid: ReturnType + const loop = () => { + if (Math.random() > skipChance) ref.current() + tid = setTimeout(loop, min + Math.random() * (max - min)) + } + loop() + return () => clearTimeout(tid) + }, [min, max, skipChance]) +} + +const MONO: React.CSSProperties = { + fontFamily: '"JetBrains Mono","SF Mono",ui-monospace,monospace', + fontVariantNumeric: 'tabular-nums', +} + +function TickerShell({ color, children }: { color: string; children: React.ReactNode }) { return ( -
- - - {n.toLocaleString('de-DE')} tok/s - +
{children}
+ ) +} + +function Caret({ color }: { color: string }) { + return ( + + ) +} + +// ── Per-node live tickers ───────────────────────────────────────────────────── +function TickCertifAI({ color }: { color: string }) { + const [n, setN] = useState(8421) + const [hash, setHash] = useState('9f3a…e10b') + const pool = 'abcdef0123456789' + const r = (k: number) => Array.from({ length: k }, () => pool[Math.floor(Math.random() * pool.length)]).join('') + useTicker(() => { setN(v => v + 1); setHash(`${r(4)}…${r(4)}`) }, 1000, 2000, 0.1) + return ( + + + sig + {n.toLocaleString()} + {hash} + + ) +} + +function TickComplAI({ color }: { color: string }) { + const [evals, setEvals] = useState(1284) + const [rate, setRate] = useState(99.2) + useTicker(() => { + setEvals(v => v + 1 + Math.floor(Math.random() * 3)) + setRate(r => Math.max(97, Math.min(99.9, r + (Math.random() - 0.5) * 0.4))) + }, 200, 500, 0.1) + return ( + + + eval + {evals.toLocaleString()} + pass + {rate.toFixed(1)}% + + ) +} + +function TickScanner({ color }: { color: string }) { + const lines = [ + { k: 'PASS', c: '#4ade80', t: 'CWE-79 xss check' }, + { k: 'WARN', c: '#fbbf24', t: 'drift: model v2.1→2.2' }, + { k: 'PASS', c: '#4ade80', t: 'bias: demographic parity' }, + { k: 'FAIL', c: '#f87171', t: 'license: GPL-3 detected' }, + { k: 'PASS', c: '#4ade80', t: 'prompt-inject: 214 vectors' }, + { k: 'SCAN', c: '#a78bfa', t: 'artifact model-card.json' }, + ] + const [i, setI] = useState(0) + useTicker(() => setI(x => (x + 1) % lines.length), 700, 1200, 0.05) + const l = lines[i] + return ( + + {l.k} + {l.t} + + ) +} + +function TickLiteLLM({ color }: { color: string }) { + const [rps, setRps] = useState(428) + const [p50, setP50] = useState(84) + useTicker(() => { + setRps(v => Math.max(200, Math.min(800, v + (Math.random() - 0.5) * 60))) + setP50(v => Math.max(40, Math.min(160, v + (Math.random() - 0.5) * 20))) + }, 250, 500, 0.05) + return ( + + + req/s + {Math.round(rps)} + · + p50 + {Math.round(p50)}ms + + + ) +} + +function TickLLM({ color }: { color: string }) { + const [tokens, setTokens] = useState(14832) + const [stream, setStream] = useState('t_a91f') + const pool = 'abcdef0123456789' + useTicker(() => { + setTokens(v => v + 1 + Math.floor(Math.random() * 5)) + setStream('t_' + Array.from({ length: 4 }, () => pool[Math.floor(Math.random() * pool.length)]).join('')) + }, 120, 340, 0.15) + return ( + + + tok + {tokens.toLocaleString()} + + {stream} + + + ) +} + +function TickEmbeddings({ color }: { color: string }) { + const [vecs, setVecs] = useState(284112) + useTicker(() => setVecs(v => v + 1 + Math.floor(Math.random() * 8)), 180, 420, 0.1) + return ( + + + idx + {vecs.toLocaleString()} + · 1024d + + + ) +} + +function TickTools({ color }: { color: string }) { + const ops = [ + 'search("BSI C5 controls")', 'fetch eur-lex.europa.eu', + 'grep -r "DSGVO"', 'read docs/policy.md', + 'mcp.call(filesystem)', 'search("vLLM 0.6 release")', + ] + const [i, setI] = useState(0) + useTicker(() => setI(x => (x + 1) % ops.length), 900, 1600, 0.05) + return ( + + + call + {ops[i]} + + ) +} + +const NODE_TICKER: Record> = { + certifai: TickCertifAI, + complai: TickComplAI, + scanner: TickScanner, + litellm: TickLiteLLM, + llm: TickLLM, + embeddings: TickEmbeddings, + tools: TickTools, +} + +// ── Animated connector between layers ──────────────────────────────────────── +function LayerConnector({ tint }: { tint: string }) { + const tracks = [ + { x: '32%', primary: false }, + { x: '50%', primary: true }, + { x: '68%', primary: false }, + ] + return ( +
+ {tracks.map(({ x, primary }, ti) => { + const color = primary ? '#fbbf24' : tint + const dots = primary ? 4 : 3 + const dur = primary ? 1.6 : 2.4 + return ( +
+ {/* Rail */} +
+ {/* Staggered dots */} + {Array.from({ length: dots }, (_, j) => ( +
+ ))} +
+ ) + })}
) } -// ── Data-flow packets along metro tracks ────────────────────────────────────── -function DataFlow({ d, revD, color, active, type }: { - d: string; revD?: string; color: string; active: boolean; type: ConnType +// ── Single node card ────────────────────────────────────────────────────────── +function NodeCard({ + node, selected, onClick, +}: { + node: NodeDef; selected: boolean; onClick: () => void }) { - 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 + const [hover, setHover] = useState(false) + const active = hover || selected + const c = node.color + const Ticker = NODE_TICKER[node.id] + const Icon = node.icon return ( - - {/* Track rail */} - - {/* Primary packet */} - - {/* Staggered trailing packet when active */} - {active && ( - + ) } -// ── 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 +// ── 3D perspective slab ─────────────────────────────────────────────────────── +function LayerSlab({ + label, sublabel, nodes, tint, depth, selectedId, onSelect, +}: { + label: string; sublabel: string; nodes: NodeDef[] + tint: string; depth: number + selectedId: NodeId | null; onSelect: (id: NodeId) => void }) { - const Icon = node.icon - const isHub = node.id === 'litellm' - const sz = isHub ? 82 : 52 - + const isProxy = nodes.length === 1 && !!nodes[0].primary return ( -
- - {/* Station circle */} -
- {/* Outer pulse ring on active */} - {active && ( - - )} - {/* Hub ambient glow */} - {isHub && ( -
- )} - {/* Main circle */} -
- + {/* Top edge highlight */} +
+ {/* Layer label row */} +
+
{label}
+
{sublabel}
+
+ {/* Cards */} +
+ {isProxy ? ( +
+ onSelect(nodes[0].id)} />
- {/* Hub spinning dashed ring */} - {isHub && active && ( - - )} - {/* Active indicator dot */} - {active && ( -
- )} -
- - {/* Station label */} -
-
- {node.title} -
-
- {node.subtitle} -
- {node.badge && ( -
- {node.badge} -
- )} - {active && node.tier === 'inference' && ( -
- )} -
- + ) : ( + nodes.map(n => ( + onSelect(n.id)} /> + )) + )} +
) } // ── Main slide ──────────────────────────────────────────────────────────────── export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { - const i = t(lang) - const de = lang === 'de' - const nodes = getNodes(de) + const i = t(lang) + const de = lang === 'de' + const allNodes = getNodes(de) + const nodeMap = Object.fromEntries(allNodes.map(n => [n.id, n])) as Record const [activeId, setActiveId] = useState(null) - const active = nodes.find(n => n.id === activeId) ?? null - function toggle(id: NodeId) { setActiveId(prev => prev === id ? null : id) } + const active = activeId ? nodeMap[activeId] : null 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: 0–36% (y=0–173), Gateway: 36–65% (y=173–312), Inference: 65–100% - const SEP1 = '36%' - const SEP2 = '65%' + const layerLabels = de + ? ['01 · Anwendung', '02 · Gateway', '03 · Infrastruktur'] + : ['01 · Application', '02 · Gateway', '03 · Infrastructure'] + const layerSublabels = de + ? ['Benutzeroberflächen', 'Routing & Guardrails', 'Compute & Daten'] + : ['User-facing services', 'Routing & guardrails', 'Compute & data'] return (
+ + {/* Header */}

@@ -394,13 +513,13 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { {i.annex.architecture.title}

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

- + {/* Customer namespace strip */} -
+
{de ? 'Kundenmandanten' : 'Customer Namespaces'} @@ -413,268 +532,183 @@ export default function ArchitectureSlide({ lang }: ArchitectureSlideProps) { ))}
- {/* ── METRO MAP ──────────────────────────────────────────────── */} -
+ {/* ── MAIN CANVAS ─────────────────────────────────────────────── */} +
- {/* Background */} -
-
-
+ {/* Ambient glows */} +
+
- {/* Tier band fills */} -
-
-
- - {/* Tier separator lines */} - {[SEP1, SEP2].map(y => ( -
- ))} - - {/* 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'} - -
-
-
- - {/* MCP label */} -
- - ⇌ MCP - -
- - {/* ── SVG: metro tracks + animated data flows ── */} - - - {/* Horizontal metro line — Application layer (y=106) */} - - {/* Station tick marks on app line */} - {[195, 550, 905].map(x => ( - - ))} - - {/* Horizontal metro line — Inference layer (y=384) */} - - {/* 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: 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 }) => ( - - ))} - - {/* 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' + {/* Slabs + connectors */} +
+ {LAYERS.map((layer, li) => { + const nodes = layer.nodeIds.map(id => nodeMap[id]) return ( - + + + {li < LAYERS.length - 1 && } + ) })} - +
- {/* Metro station nodes */} - {nodes.map(node => ( - toggle(node.id)} - de={de} - /> - ))} - - {!activeId && ( -
- - {de ? 'Station anklicken' : 'Click any station'} -
- )} -
- - {/* ── DETAIL PANEL ─────────────────────────────────────────────── */} - - {active && ( - - - -
-
- -
-
-
-

{active.title}

- {active.badge && ( - - {active.badge} - - )} - - {active.tier === 'product' ? (de ? 'Anwendungsschicht' : 'Application Layer') : - active.tier === 'proxy' ? (de ? 'GenAI-Infrastruktur' : 'GenAI Infra') : - (de ? 'Inferenzschicht' : 'Inference Layer')} - -
-

{active.subtitle}

-
+ {/* Footer badges */} +
+ {([ + { Icon: Lock, label: de ? 'Kein US-Anbieter · 100% DSGVO' : 'No US providers · 100% GDPR' }, + { Icon: Server, label: de ? 'BSI-zertifiziertes Rechenzentrum' : 'BSI-certified data center' }, + { Icon: BadgeCheck, label: de ? 'EU-souveräne Inferenz' : 'EU-sovereign inference' }, + ] as { Icon: React.ElementType; label: string }[]).map(({ Icon, label }) => ( +
+ + {label}
+ ))} +
-
-
-

- {de ? 'Stack' : 'Tech Stack'} -

-
- {active.tech.map(tk => ( - - {tk} - - ))} -
-
-
-

- {de ? 'Funktionen' : 'Capabilities'} -

-
- {active.services.map(s => ( -
-
-
- {s.name} - {s.desc} + {/* ── Detail panel: slides up from bottom ── */} + + {active && ( + +
+ {/* Panel header */} +
+
+
+ +
+
+
+ + {active.title} + + + {active.tier === 'product' ? (de ? 'Anwendung' : 'Application') : + active.tier === 'proxy' ? 'Gateway' : + (de ? 'Inferenz' : 'Inference')} + +
+
+ {active.subtitle}
- ))} +
+ +
+ {/* Tech + capabilities grid */} +
+
+
+ {de ? 'Stack' : 'Tech Stack'} +
+
+ {active.tech.map(tk => ( + {tk} + ))} +
+
+
+
+ {de ? 'Funktionen' : 'Capabilities'} +
+
+ {active.services.map(s => ( +
+
+ {s.name} + {s.desc} +
+ ))} +
+
-
- -
- - - {de ? 'Verbunden mit:' : 'Connects to:'} - - {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 ( - - ) - })} -
-
- )} -
- - {/* 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: BadgeCheck, text: de ? 'EU-souveräne Inferenz' : 'EU-sovereign inference' }, - ].map(({ icon: Icon, text }) => ( - - {text} - - ))} - - )} + + )} + +
)