Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>VECTOR 2026 — Voice-Enabled Multilingual RAG Engine</title> | |
| <!-- Fonts (Creative & Stylish Beach Pop Art & Logo Typography) --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Luckiest+Guy&family=Righteous&family=Outfit:wght@400;600;700;800;900&family=Bangers&family=Comic+Neue:ital,wght@0,400;0,700;1,400;1,700&family=Fredoka:wght@400;600;700&family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Rozha+One&family=Yatra+One&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" /> | |
| <!-- Tailwind CSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| beachgold: '#FFC107', | |
| beachcyan: '#00B4D8', | |
| beachpink: '#FF007F', | |
| beachgreen: '#0A5C36', | |
| palmemerald: '#0A5C36', | |
| sunyellow: '#FFC107', | |
| surfpink: '#FF007F', | |
| oceanemerald: '#053B22', | |
| beachblue: '#0077B6', | |
| beachsand: '#FFFBEA', | |
| sand: { | |
| 50: '#FFFFFF', | |
| 100: '#FFFBEA', | |
| 200: '#FFF3B0', | |
| 300: '#FFE082', | |
| 400: '#FFC107', | |
| }, | |
| jungle: { | |
| 950: '#031F13', | |
| 900: '#05331F', | |
| 800: '#0A5C36', | |
| 700: '#0E7C4A', | |
| }, | |
| hhgold: '#FFC107', | |
| hhpink: '#FF007F', | |
| hhemerald: '#0A5C36', | |
| hhdarkgreen: '#042917', | |
| }, | |
| fontFamily: { | |
| pop: ['"Luckiest Guy"', '"Bangers"', 'cursive', 'sans-serif'], | |
| righteous: ['"Righteous"', 'sans-serif'], | |
| outfit: ['"Outfit"', 'sans-serif'], | |
| comic: ['"Bangers"', 'cursive', 'sans-serif'], | |
| comicbody: ['"Comic Neue"', 'cursive', 'sans-serif'], | |
| fredoka: ['"Fredoka"', 'sans-serif'], | |
| display: ['"Luckiest Guy"', '"Righteous"', '"Bangers"', 'serif'], | |
| serif: ['"Outfit"', '"Comic Neue"', 'sans-serif'], | |
| mono: ['"Space Mono"', 'monospace'], | |
| sans: ['"Outfit"', '"Plus Jakarta Sans"', 'sans-serif'], | |
| }, | |
| boxShadow: { | |
| 'brutal': '4px 4px 0px #000000', | |
| 'brutal-lg': '6px 6px 0px #000000', | |
| 'brutal-sm': '3px 3px 0px #000000', | |
| 'brutal-pink': '4px 4px 0px #FF007F', | |
| 'brutal-gold': '4px 4px 0px #FFC107', | |
| 'brutal-green': '4px 4px 0px #0A5C36', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| :root { | |
| color-scheme: light ; | |
| } | |
| html, body { | |
| color-scheme: light ; | |
| background-color: #0A5C36 ; | |
| color: #111827 ; | |
| font-family: 'Outfit', 'Comic Neue', sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| min-height: 100vh; | |
| } | |
| /* Creative & Stylish 3D Pop Art Typography */ | |
| .font-pop-art { | |
| font-family: 'Luckiest Guy', 'Bangers', cursive; | |
| letter-spacing: 0.05em; | |
| color: #FFC107; | |
| -webkit-text-stroke: 1.5px #000000; | |
| text-shadow: | |
| 3px 3px 0px #000000, | |
| 5px 5px 0px #FF007F; | |
| } | |
| .font-stylish-title { | |
| font-family: 'Righteous', sans-serif; | |
| letter-spacing: 0.04em; | |
| text-shadow: 2px 2px 0px #000000; | |
| } | |
| .pop-text-highlight { | |
| position: relative; | |
| display: inline-block; | |
| font-family: 'Luckiest Guy', 'Bangers', cursive; | |
| letter-spacing: 0.04em; | |
| color: #000000; | |
| } | |
| .pop-text-highlight::after { | |
| content: ''; | |
| position: absolute; | |
| left: -2px; | |
| bottom: 1px; | |
| width: calc(100% + 4px); | |
| height: 12px; | |
| background-color: #FFC107; | |
| z-index: -1; | |
| border-radius: 4px; | |
| transform: rotate(-1deg); | |
| } | |
| /* Force high contrast text on sand backgrounds */ | |
| .bg-sand-50, .bg-sand-100, .bg-sand-200, .bg-sand-300, .bg-sand-400 { | |
| color: #111827 ; | |
| } | |
| .text-black { | |
| color: #000000 ; | |
| } | |
| .text-slate-800, .text-slate-700, .text-slate-600 { | |
| color: #1F2937 ; | |
| } | |
| /* Dark containers in Knowledge Sea */ | |
| .sea-grid, .bg-jungle-950, .bg-jungle-900, .bg-jungle-800 { | |
| color: #FEF8EA ; | |
| } | |
| .sea-grid .bg-sand-100 { | |
| background-color: #FEF8EA ; | |
| color: #111827 ; | |
| } | |
| .sea-grid .bg-sand-100 p, .sea-grid .bg-sand-100 span, .sea-grid .bg-sand-100 div { | |
| color: #111827 ; | |
| } | |
| /* Beach Comic Typography & Panel Styling */ | |
| .comic-title { | |
| font-family: 'Luckiest Guy', 'Bangers', cursive; | |
| letter-spacing: 0.05em; | |
| text-shadow: 2px 2px 0px #000; | |
| } | |
| .comic-panel { | |
| border: 3.5px solid #000 ; | |
| box-shadow: 6px 6px 0px #000 ; | |
| border-radius: 14px; | |
| background: #FFFFFF; | |
| } | |
| .comic-badge { | |
| font-family: 'Righteous', 'Bangers', cursive; | |
| letter-spacing: 0.05em; | |
| border: 2.5px solid #000; | |
| box-shadow: 3px 3px 0px #000; | |
| text-transform: uppercase; | |
| } | |
| /* Comic Action Burst Pill */ | |
| .action-burst { | |
| position: relative; | |
| display: inline-block; | |
| font-family: 'Luckiest Guy', 'Bangers', cursive; | |
| background: #FF006E; | |
| color: #FFFFFF; | |
| padding: 2px 10px; | |
| border: 2.5px solid #000; | |
| box-shadow: 3px 3px 0px #000; | |
| transform: rotate(-2deg); | |
| letter-spacing: 0.05em; | |
| border-radius: 6px; | |
| } | |
| /* Right to Left Continuous Ticker Marquee */ | |
| @keyframes marqueeScroll { | |
| 0% { transform: translateX(0%); } | |
| 100% { transform: translateX(-50%); } | |
| } | |
| .ticker-container { | |
| overflow: hidden; | |
| white-space: nowrap; | |
| position: relative; | |
| width: 100%; | |
| mask-image: linear-gradient(to right, transparent 0%, black 3%, black 97%, transparent 100%); | |
| -webkit-mask-image: linear-gradient(to right, transparent 0%, black 3%, black 97%, transparent 100%); | |
| } | |
| .ticker-track { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 12px; | |
| animation: marqueeScroll 30s linear infinite; | |
| width: max-content; | |
| } | |
| .ticker-container:hover .ticker-track { | |
| animation-play-state: paused; | |
| } | |
| .hover-pop { | |
| transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), box-shadow 0.2s ease; | |
| } | |
| .hover-pop:hover { | |
| transform: translateY(-2px) scale(1.04) rotate(1deg); | |
| box-shadow: 5px 5px 0px #000000 ; | |
| } | |
| /* Asymmetrical Ocean Wave Telemetry Console Shape - 100% Solid Opaque */ | |
| .shape-telemetry-console { | |
| clip-path: polygon(0% 0%, 100% 0%, 100% calc(100% - 22px), calc(100% - 26px) 100%, 0% 100%); | |
| border: 3.5px solid #000; | |
| box-shadow: 7px 7px 0px #FFC107; | |
| background-color: #053B22 ; | |
| } | |
| /* Surfboard Shield Guardrail Matrix Shape - 100% Solid Opaque */ | |
| .shape-guardrail-matrix { | |
| position: relative; | |
| z-index: 10; | |
| border-radius: 36px 12px 36px 12px; | |
| border: 3.5px solid #000; | |
| box-shadow: 7px 7px 0px #FF007F; | |
| background-color: #FFFBEA ; | |
| opacity: 1 ; | |
| backdrop-filter: none ; | |
| -webkit-backdrop-filter: none ; | |
| } | |
| .ocean-dark-bg { | |
| background-color: #053B22 ; | |
| color: #FFFFFF ; | |
| } | |
| .beach-sand-bg { | |
| background-color: #FFFBEA ; | |
| color: #111827 ; | |
| opacity: 1 ; | |
| } | |
| /* Ocean Knowledge Sea Deck Shape - 100% Solid Opaque */ | |
| .shape-knowledge-sea { | |
| clip-path: polygon(0% 0%, 100% 0%, 100% calc(100% - 22px), calc(100% - 26px) 100%, 0% 100%); | |
| border: 3.5px solid #000; | |
| box-shadow: 7px 7px 0px #FFC107; | |
| background-color: #053B22 ; | |
| } | |
| /* Grounded Answer Hero Panel Shape - 100% Solid Opaque */ | |
| .shape-grounded-hero { | |
| border-radius: 28px 10px 28px 10px; | |
| border: 3.5px solid #000; | |
| box-shadow: 7px 7px 0px #FF007F; | |
| background-color: #FFFFFF ; | |
| } | |
| .ocean-deck-bg { | |
| background-color: #053B22 ; | |
| color: #FFFFFF ; | |
| } | |
| .wave-badge-glow { | |
| box-shadow: 0 0 12px rgba(0, 180, 216, 0.4); | |
| } | |
| /* Speech Bubble Style */ | |
| .speech-bubble { | |
| position: relative; | |
| background: #FFFFFF; | |
| border: 3.5px solid #000; | |
| box-shadow: 6px 6px 0px #000; | |
| border-radius: 16px; | |
| } | |
| /* Retro Underline Highlight */ | |
| .title-highlight { | |
| position: relative; | |
| display: inline-block; | |
| font-family: 'Luckiest Guy', 'Bangers', cursive; | |
| letter-spacing: 0.05em; | |
| } | |
| .title-highlight::after { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| bottom: 2px; | |
| width: 100%; | |
| height: 10px; | |
| background-color: #FFD166; | |
| z-index: -1; | |
| border-radius: 3px; | |
| } | |
| /* Knowledge Sea Wave Grid */ | |
| .sea-grid { | |
| background-color: #071813; | |
| background-image: | |
| radial-gradient(rgba(0, 229, 153, 0.15) 1.5px, transparent 1.5px), | |
| linear-gradient(rgba(0, 180, 216, 0.1) 1px, transparent 1px); | |
| background-size: 24px 24px, 24px 24px; | |
| } | |
| /* Retro Stamp Watermark */ | |
| .stamp-watermark { | |
| background-image: radial-gradient(circle, rgba(254, 248, 234, 0.3) 1px, transparent 1px); | |
| background-size: 6px 6px; | |
| } | |
| /* Custom Scrollbars */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #FFF3B0; | |
| border-left: 2px solid #000; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #FFD166; | |
| border: 2px solid #000; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #FF006E; | |
| } | |
| /* Star Shape & Spider Web UI Styling */ | |
| .clip-star-8 { | |
| clip-path: polygon(50% 0%, 61.5% 28.8%, 85.4% 14.6%, 71.2% 38.5%, 100% 50%, 71.2% 61.5%, 85.4% 85.4%, 61.5% 71.2%, 50% 100%, 38.5% 71.2%, 14.6% 85.4%, 28.8% 61.5%, 0% 50%, 28.8% 38.5%, 14.6% 14.6%, 38.5% 28.8%); | |
| } | |
| .clip-star-5 { | |
| clip-path: polygon(50% 0%, 61.8% 35%, 98.2% 35%, 68.7% 56.2%, 80% 91.2%, 50% 69.4%, 20% 91.2%, 31.3% 56.2%, 1.8% 35%, 38.2% 35%); | |
| } | |
| @keyframes webGlow { | |
| 0%, 100% { filter: drop-shadow(0 0 8px rgba(0, 180, 216, 0.7)); } | |
| 50% { filter: drop-shadow(0 0 16px rgba(255, 0, 110, 0.95)); } | |
| } | |
| .animate-web-glow { | |
| animation: webGlow 3s infinite ease-in-out; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col relative selection:bg-sunyellow selection:text-black"> | |
| <!-- Full-Page Flying Iron Man Canvas Layer (Behind Frame Boxes) --> | |
| <canvas id="ironmanMainCanvas" class="fixed inset-0 w-full h-full pointer-events-none z-1"></canvas> | |
| <!-- TROPICAL EMERALD BEACH VECTOR ARTWORK BACKGROUND (MATCHES REFERENCE IMAGE) --> | |
| <div class="fixed inset-0 pointer-events-none z-0 overflow-hidden flex items-center justify-center"> | |
| <svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" class="w-full h-full opacity-45 md:opacity-55"> | |
| <defs> | |
| <radialGradient id="sunGlow" cx="50%" cy="50%" r="50%"> | |
| <stop offset="0%" stop-color="#FFE082" stop-opacity="1" /> | |
| <stop offset="60%" stop-color="#FFC107" stop-opacity="0.9" /> | |
| <stop offset="100%" stop-color="#FFC107" stop-opacity="0" /> | |
| </radialGradient> | |
| </defs> | |
| <!-- Deep Emerald Sky & Sea Background Base --> | |
| <rect width="1440" height="900" fill="#0A5C36" /> | |
| <!-- Golden Sunset Orb --> | |
| <circle cx="720" cy="460" r="250" fill="#FFC107" /> | |
| <circle cx="720" cy="460" r="280" fill="url(#sunGlow)" /> | |
| <!-- Sailboat Silhouette --> | |
| <g stroke="#FFFFFF" stroke-width="2" fill="#053B22"> | |
| <path d="M 850,540 L 890,540 L 880,555 L 860,555 Z" stroke-width="2" /> | |
| <path d="M 870,540 L 870,490 L 895,535 Z" stroke-width="2" /> | |
| </g> | |
| <!-- Sea Horizon Line & Waves --> | |
| <line x1="0" y1="560" x2="1440" y2="560" stroke="#FFFFFF" stroke-width="3.5" /> | |
| <path d="M 50,580 Q 80,573 110,580 T 170,580" stroke="#FFFFFF" stroke-width="2.5" fill="none" /> | |
| <path d="M 300,610 Q 340,602 380,610 T 460,610" stroke="#FFFFFF" stroke-width="2.5" fill="none" /> | |
| <path d="M 580,640 Q 620,632 660,640 T 740,640" stroke="#FFFFFF" stroke-width="2.5" fill="none" /> | |
| <path d="M 850,600 Q 890,592 930,600 T 1010,600" stroke="#FFFFFF" stroke-width="2.5" fill="none" /> | |
| <path d="M 1100,620 Q 1140,612 1180,620 T 1260,620" stroke="#FFFFFF" stroke-width="2.5" fill="none" /> | |
| <!-- Flying Seagulls in Sky --> | |
| <g stroke="#FFFFFF" stroke-width="3" fill="none" stroke-linecap="round"> | |
| <path d="M 480,190 Q 495,175 510,190 Q 525,175 540,190" /> | |
| <path d="M 440,270 Q 452,258 464,270 Q 476,258 488,270" /> | |
| <path d="M 900,250 Q 912,238 924,250 Q 936,238 948,250" /> | |
| <path d="M 1010,290 Q 1020,280 1030,290 Q 1040,280 1050,290" /> | |
| </g> | |
| <!-- Fluffy Clouds --> | |
| <g stroke="#FFFFFF" stroke-width="3" fill="none"> | |
| <path d="M 200,360 C 200,330 240,320 260,340 C 280,320 330,330 340,360 Z" /> | |
| <path d="M 970,140 C 970,115 1000,105 1015,120 C 1030,105 1070,115 1080,140 Z" /> | |
| </g> | |
| <!-- Left Palm Trees (Clean White Line Art) --> | |
| <g stroke="#FFFFFF" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"> | |
| <!-- Main Left Trunk --> | |
| <path d="M 30,900 Q 120,500 200,250" /> | |
| <path d="M 55,900 Q 140,510 215,255" /> | |
| <!-- Fronds Left Palm --> | |
| <path d="M 205,250 Q 130,120 30,150 Q 90,200 205,250" fill="#0A5C36" /> | |
| <path d="M 205,250 Q 230,100 340,110 Q 270,170 205,250" fill="#0A5C36" /> | |
| <path d="M 205,250 Q 300,220 360,280 Q 270,280 205,250" fill="#0A5C36" /> | |
| <path d="M 205,250 Q 100,270 50,320 Q 130,320 205,250" fill="#0A5C36" /> | |
| <!-- Second Palm Tree Trunk & Fronds --> | |
| <path d="M -20,900 Q 80,450 180,100" /> | |
| <path d="M 180,100 Q 80,10 20,30 Q 80,70 180,100" fill="#0A5C36" /> | |
| <path d="M 180,100 Q 220,0 310,20 Q 240,60 180,100" fill="#0A5C36" /> | |
| <path d="M 180,100 Q 270,100 320,160 Q 240,150 180,100" fill="#0A5C36" /> | |
| </g> | |
| <!-- Right Palm Tree (Clean White Line Art) --> | |
| <g stroke="#FFFFFF" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M 1430,900 Q 1350,480 1260,180" /> | |
| <path d="M 1410,900 Q 1330,490 1245,185" /> | |
| <path d="M 1255,180 Q 1350,60 1440,80 Q 1370,130 1255,180" fill="#0A5C36" /> | |
| <path d="M 1255,180 Q 1200,40 1100,70 Q 1170,120 1255,180" fill="#0A5C36" /> | |
| <path d="M 1255,180 Q 1120,170 1060,230 Q 1150,230 1255,180" fill="#0A5C36" /> | |
| <path d="M 1255,180 Q 1340,240 1410,310 Q 1330,300 1255,180" fill="#0A5C36" /> | |
| </g> | |
| <!-- Hot Magenta Surfboard (Right Foreground) --> | |
| <g stroke="#FFFFFF" stroke-width="3.5"> | |
| <path d="M 1020,510 C 990,600 985,800 1015,880 L 1070,880 C 1100,800 1095,600 1065,510 C 1045,450 1040,450 1020,510 Z" fill="#FF007F" /> | |
| <line x1="1042" y1="460" x2="1042" y2="880" stroke="#FFFFFF" stroke-width="3" /> | |
| </g> | |
| <!-- Striped Beach Umbrella (Left Foreground) --> | |
| <g stroke="#000000" stroke-width="3.5"> | |
| <path d="M 50,560 A 200,160 0 0,1 450,560 Z" fill="#FFC107" stroke="#FFFFFF" stroke-width="3.5" /> | |
| <path d="M 110,560 A 200,160 0 0,1 190,560 L 250,405 Z" fill="#FF007F" stroke="#FFFFFF" stroke-width="3" /> | |
| <path d="M 270,560 A 200,160 0 0,1 350,560 L 250,405 Z" fill="#FF007F" stroke="#FFFFFF" stroke-width="3" /> | |
| <line x1="250" y1="400" x2="310" y2="860" stroke="#FFFFFF" stroke-width="6" /> | |
| </g> | |
| <!-- Striped Beach Ball --> | |
| <g stroke="#FFFFFF" stroke-width="3"> | |
| <circle cx="570" cy="830" r="50" fill="#FFC107" /> | |
| <path d="M 520,830 A 50,50 0 0,0 620,830 Z" fill="#FF007F" /> | |
| <path d="M 570,780 A 50,50 0 0,0 570,880 Z" fill="#FFFFFF" /> | |
| </g> | |
| <!-- Bottom White Sand Beach Shore Line --> | |
| <path d="M 0,660 Q 250,630 500,720 T 1000,730 T 1440,670 L 1440,900 L 0,900 Z" fill="#FFFFFF" fill-opacity="0.14" stroke="#FFFFFF" stroke-width="3" /> | |
| </svg> | |
| </div> | |
| <!-- TOP BEACH COMIC GOA HEADER STRIP --> | |
| <header class="w-full bg-sunyellow border-b-4 border-black shadow-brutal relative z-10"> | |
| <!-- Top Branding Strip --> | |
| <div class="w-full bg-gradient-to-r from-emerald-950 via-green-900 to-emerald-900 border-b-3 border-black px-4 md:px-8 py-3 flex flex-wrap items-center justify-between gap-4"> | |
| <!-- Left: Official Logo & Event Title --> | |
| <div class="flex items-center gap-4"> | |
| <!-- HACKER HOUSE गोवा Official Logo Asset --> | |
| <div class="w-28 md:w-36 h-16 md:h-20 bg-palmemerald border-3 border-black rounded-xl p-1 shadow-brutal transform -rotate-2 hover:rotate-3 transition-transform flex items-center justify-center overflow-hidden"> | |
| <svg viewBox="0 0 500 300" class="w-full h-full"> | |
| <defs> | |
| <style> | |
| .logo-text-hh { font-family: 'Rozha One', 'Cinzel Decorative', serif; font-size: 112px; font-weight: 900; fill: #FFC107; letter-spacing: -2px; text-anchor: middle; } | |
| .logo-text-goa { font-family: 'Yatra One', 'Rozha One', 'Devanagari', serif; font-size: 105px; font-weight: 900; fill: #FF007F; stroke: #FFC107; stroke-width: 5px; paint-order: stroke fill; text-anchor: middle; filter: drop-shadow(4px 4px 0px #000000); } | |
| </style> | |
| </defs> | |
| <rect width="500" height="300" rx="16" fill="#0A5C36" /> | |
| <text x="250" y="115" class="logo-text-hh" transform="scale(1, 1.15)" transform-origin="250 115">HACKER</text> | |
| <text x="250" y="245" class="logo-text-hh" transform="scale(1, 1.15)" transform-origin="250 245">HOUSE</text> | |
| <g transform="translate(250, 168)"> | |
| <text x="0" y="24" class="logo-text-goa">गोवा</text> | |
| </g> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="flex items-center gap-2"> | |
| <span class="action-burst text-xs">VECTOR 2026</span> | |
| <span class="px-2 py-0.5 bg-black text-beachcyan font-comic text-xs border-2 border-black rounded shadow-brutal-sm">POP ART EDITION</span> | |
| </div> | |
| <h1 class="font-pop-art text-3xl md:text-4xl tracking-wide mt-0.5">VOICE INDIC RAG 🌊 SURF THE VECTORS! 🏄♂️</h1> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Controls Toolbar (Continuous Right-to-Left Ticker Marquee) --> | |
| <div class="w-full py-2.5 bg-yellow-100 border-b-3 border-black overflow-hidden relative flex items-center"> | |
| <div class="ticker-container"> | |
| <div class="ticker-track"> | |
| <!-- First Set of Languages --> | |
| <button data-lang="en" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-black text-beachgold border-2.5 border-black shadow-brutal-sm hover-pop">ENGLISH 🇬🇧</button> | |
| <button data-lang="hi" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">हिन्दी 🇮🇳</button> | |
| <button data-lang="mr" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">मराठी 🇮🇳</button> | |
| <button data-lang="kn" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ಕನ್ನಡ 🇮🇳</button> | |
| <button data-lang="te" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">తెలుగు 🇮🇳</button> | |
| <button data-lang="ta" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">தமிழ் 🇮🇳</button> | |
| <button data-lang="bn" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">বাংলা 🇮🇳</button> | |
| <button data-lang="gu" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ગુજરાતી 🇮🇳</button> | |
| <button data-lang="pa" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ਪੰਜਾਬੀ 🇮🇳</button> | |
| <button data-lang="ml" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">മലയാളം 🇮🇳</button> | |
| <button data-lang="or" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ଓଡ଼ିଆ 🇮🇳</button> | |
| <button data-lang="as" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">অসমীয়া 🇮🇳</button> | |
| <button data-lang="ur" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">اردو 🇵🇰</button> | |
| <button data-lang="ne" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">नेपाली 🇳🇵</button> | |
| <!-- Duplicate Loop Set for Seamless Right-to-Left Marquee --> | |
| <button data-lang="en" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-black text-beachgold border-2.5 border-black shadow-brutal-sm hover-pop">ENGLISH 🇬🇧</button> | |
| <button data-lang="hi" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">हिन्दी 🇮🇳</button> | |
| <button data-lang="mr" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">मराठी 🇮🇳</button> | |
| <button data-lang="kn" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ಕನ್ನಡ 🇮🇳</button> | |
| <button data-lang="te" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">తెలుగు 🇮🇳</button> | |
| <button data-lang="ta" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">தமிழ் 🇮🇳</button> | |
| <button data-lang="bn" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">বাংলা 🇮🇳</button> | |
| <button data-lang="gu" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ગુજરાતી 🇮🇳</button> | |
| <button data-lang="pa" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ਪੰਜਾਬੀ 🇮🇳</button> | |
| <button data-lang="ml" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">മലയാളം 🇮🇳</button> | |
| <button data-lang="or" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">ଓଡ଼ିଆ 🇮🇳</button> | |
| <button data-lang="as" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">অসমীয়া 🇮🇳</button> | |
| <button data-lang="ur" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">اردو 🇵🇰</button> | |
| <button data-lang="ne" class="lang-btn px-3.5 py-1 rounded-xl text-xs font-comic font-bold transition bg-white text-black border-2.5 border-black shadow-brutal-sm hover-pop hover:bg-beachgold">नेपाली 🇳🇵</button> | |
| </div> | |
| </div> | |
| <span id="detectedLangPill" class="hidden absolute right-4 px-2.5 py-1 rounded-lg text-xs font-comic bg-black text-beachgreen border-2 border-black shadow-brutal-sm z-20">ROUTED: EN</span> | |
| </div> | |
| </header> | |
| <!-- MAIN WORKSPACE (THE TERMINAL & THE KNOWLEDGE SEA) --> | |
| <main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 flex flex-col gap-6 relative z-10"> | |
| <!-- DUAL PANE WORKBENCH --> | |
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start"> | |
| <!-- LEFT: THE TERMINAL --> | |
| <div class="lg:col-span-5 flex flex-col gap-4"> | |
| <div class="flex items-center justify-end"> | |
| <span class="comic-badge px-3 py-1 bg-black text-beachgold text-xs shadow-brutal-sm">VOICE INTERFACE</span> | |
| </div> | |
| <!-- Terminal Container --> | |
| <div class="comic-panel p-5 flex flex-col items-center justify-center gap-4"> | |
| <!-- Spider-Man Green Mask Canvas Container (#053b22 Forest Green Theme) --> | |
| <div class="relative w-64 h-64 rounded-full bg-[#053b22] border-4 border-[#053b22] shadow-[0_0_25px_rgba(5,59,34,0.8)] flex items-center justify-center overflow-hidden my-2"> | |
| <!-- Animated Spider-Man Mask Waveform Canvas --> | |
| <canvas id="waveformCanvas" class="absolute inset-0 w-full h-full pointer-events-none"></canvas> | |
| <!-- Microphone Record Button (Only Mic Icon, Green Spider-Man Theme) --> | |
| <button id="recordBtn" class="relative z-10 p-3 flex items-center justify-center cursor-pointer hover:scale-110 active:scale-95 transition-transform duration-200 focus:outline-none"> | |
| <span id="micIcon" class="material-symbols-outlined text-5xl font-black text-black hover:text-slate-800 drop-shadow-[0_2px_6px_rgba(255,255,255,0.4)] selection:bg-transparent transition-colors duration-200">mic</span> | |
| </button> | |
| </div> | |
| <!-- Query Input Bar (Pill Shape matching reference image) --> | |
| <div class="w-full flex gap-2"> | |
| <input type="text" id="queryInput" placeholder="Ask in English, Hindi, Telugu..." | |
| class="w-full bg-sand-50 border-3 border-black px-5 py-3 font-comicbody text-base text-black placeholder:text-slate-500 focus:outline-none focus:bg-white shadow-brutal-sm rounded-full"/> | |
| <button id="searchBtn" class="px-6 py-3 bg-black hover:bg-slate-900 active:scale-95 text-beachgold font-comic font-black text-base border-3 border-black shadow-brutal-sm flex items-center gap-1.5 transition rounded-full whitespace-nowrap"> | |
| <span>GO!</span> | |
| <span class="material-symbols-outlined text-base font-black">arrow_forward</span> | |
| </button> | |
| </div> | |
| <!-- Quick Multilingual Prompt Chips (Pill Badges matching reference image) --> | |
| <div class="w-full flex flex-col gap-1.5 font-comic text-xs"> | |
| <div class="flex items-center gap-2 overflow-x-auto pb-1 max-w-full"> | |
| <button class="quick-chip px-4 py-1.5 bg-yellow-200 hover:bg-beachgold text-black border-2.5 border-black rounded-full text-xs font-bold whitespace-nowrap transition shadow-brutal-sm" data-lang="en" data-text="What was the purpose of the Manhattan Project?">ENGLISH</button> | |
| <button class="quick-chip px-4 py-1.5 bg-cyan-200 hover:bg-beachcyan text-black border-2.5 border-black rounded-full text-xs font-bold whitespace-nowrap transition shadow-brutal-sm" data-lang="hi" data-text="मैनहट्टन परियोजना का मुख्य उद्देश्य क्या था?">HINDI</button> | |
| <button class="quick-chip px-4 py-1.5 bg-pink-200 hover:bg-beachpink text-black border-2.5 border-black rounded-full text-xs font-bold whitespace-nowrap transition shadow-brutal-sm" data-lang="te" data-text="మ్యాన్హ్యాటన్ ప్రాజెక్ట్ యొక్క ముఖ్య ఉద్దేశ్యం ఏమిటి?">TELUGU</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- AUDIO FIELD NOTE (TRANSCRIPTION NOTE - COMIC SPEECH BUBBLE) --> | |
| <div id="fieldNoteCard" class="hidden speech-bubble p-4 relative overflow-hidden"> | |
| <!-- HACKER HOUSE गोवा Logo Stamp Watermark --> | |
| <div class="absolute right-3 top-3 w-14 h-10 rounded-lg border-2 border-dashed border-beachpink p-0.5 flex items-center justify-center pointer-events-none rotate-12 stamp-watermark bg-palmemerald overflow-hidden shadow-brutal-sm"> | |
| <svg viewBox="0 0 500 300" class="w-full h-full"> | |
| <defs> | |
| <style> | |
| .logo-text-hh-stamp { font-family: 'Rozha One', 'Cinzel Decorative', serif; font-size: 112px; font-weight: 900; fill: #FFC107; letter-spacing: -2px; text-anchor: middle; } | |
| .logo-text-goa-stamp { font-family: 'Yatra One', 'Rozha One', 'Devanagari', serif; font-size: 105px; font-weight: 900; fill: #FF007F; stroke: #FFC107; stroke-width: 5px; paint-order: stroke fill; text-anchor: middle; filter: drop-shadow(4px 4px 0px #000000); } | |
| </style> | |
| </defs> | |
| <rect width="500" height="300" rx="16" fill="#0A5C36" /> | |
| <text x="250" y="115" class="logo-text-hh-stamp" transform="scale(1, 1.15)" transform-origin="250 115">HACKER</text> | |
| <text x="250" y="245" class="logo-text-hh-stamp" transform="scale(1, 1.15)" transform-origin="250 245">HOUSE</text> | |
| <g transform="translate(250, 168)"> | |
| <text x="0" y="24" class="logo-text-goa-stamp">गोवा</text> | |
| </g> | |
| </svg> | |
| </div> | |
| <div class="font-comic font-black text-xs text-beachpink tracking-widest uppercase mb-1 flex items-center gap-1"> | |
| <span>AUDIO FIELD NOTE</span> | |
| <span>///</span> | |
| </div> | |
| <p id="fieldNoteText" class="font-comicbody text-lg text-black font-bold italic leading-snug"></p> | |
| </div> | |
| </div> | |
| <!-- RIGHT: THE KNOWLEDGE SEA (ASYMMETRICAL WAVE DECK SHAPE & OCEAN THEME) --> | |
| <div class="lg:col-span-7 flex flex-col gap-4"> | |
| <div class="flex items-center justify-between"> | |
| <h2 class="font-pop-art text-2xl md:text-3xl tracking-wide flex items-center gap-2"> | |
| <span>🌊 THE OCEAN KNOWLEDGE SEA 🏄♂️</span> | |
| </h2> | |
| <span id="topKBadge" class="comic-badge px-3 py-1 bg-black text-beachgreen text-xs shadow-brutal-sm">AWAITING QUERY</span> | |
| </div> | |
| <!-- Passage Stream Deck (Custom Ocean Wave Deck Shape) --> | |
| <div class="shape-knowledge-sea ocean-deck-bg p-4.5 min-h-[340px] max-h-[420px] overflow-y-auto flex flex-col justify-between"> | |
| <div id="passagesList" class="flex flex-col gap-3"> | |
| <!-- Beach Pop Art Standby State (100% Solid Opaque Box) --> | |
| <div class="p-8 text-center flex flex-col items-center justify-center gap-3.5 my-auto bg-black border-3 border-sunyellow rounded-2xl shadow-brutal p-6"> | |
| <div class="w-16 h-16 rounded-full border-3 border-black bg-sunyellow flex items-center justify-center text-black text-3xl shadow-brutal-sm"> | |
| 🏄♂️ | |
| </div> | |
| <div class="font-comic font-black text-xl text-sunyellow tracking-wider drop-shadow-md">AWAITING QUERY INPUT 🏄♂️</div> | |
| <p class="font-comicbody text-sm md:text-base text-white max-w-md leading-relaxed bg-jungle-950 p-4 rounded-xl border-2 border-sunyellow shadow-brutal-sm font-bold"> | |
| Speak into The Voice Terminal mic or enter a prompt above to retrieve grounded multi-lingual passages! | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GROUNDED ANSWER HERO CARD (SURFBOARD SHIELD SHAPE & BEACH GREEN SHADOW) --> | |
| <section class="w-full"> | |
| <div class="shape-grounded-hero p-5 md:p-6 relative bg-white"> | |
| <!-- Header --> | |
| <div class="flex flex-wrap items-center justify-between gap-2 pb-3 mb-3 border-b-3 border-black"> | |
| <div class="flex items-center gap-2 font-comic font-bold text-lg"> | |
| <span id="answerStatusIcon" class="material-symbols-outlined text-beachpink text-2xl font-black">hourglass_empty</span> | |
| <span id="answerStatusTag" class="text-black tracking-wider uppercase font-black">READY FOR INQUIRY 🏖️</span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span id="answerSourceBadge" class="comic-badge px-3 py-1 bg-black text-beachgold text-xs">STANDBY</span> | |
| <span id="retrievalTimePill" class="comic-badge px-3 py-1 bg-beachcyan text-white text-xs">LATENCY: --</span> | |
| </div> | |
| </div> | |
| <!-- Text --> | |
| <div id="answerContent" class="font-comicbody text-lg md:text-xl text-slate-800 font-bold leading-relaxed min-h-[60px] flex items-center"> | |
| Awaiting query... Speak into The Voice Terminal mic or type a prompt above to generate grounded answers! 🏄♂️ | |
| </div> | |
| </div> | |
| </section> | |
| <!-- UNIFIED COHESIVE SYS TELEMETRY & GUARDRAIL CONSOLE --> | |
| <section class="w-full"> | |
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-stretch"> | |
| <!-- LEFT: TELEMETRY CONSOLE (ASYMMETRICAL OCEAN WAVE SHAPE & NIGHT GLOW THEME) --> | |
| <div class="lg:col-span-6 shape-telemetry-console ocean-dark-bg p-5 text-white flex flex-col justify-between gap-4"> | |
| <!-- Console Header --> | |
| <div class="flex items-center justify-between border-b-2 border-sunyellow pb-2.5 font-mono text-xs flex-wrap gap-2"> | |
| <span class="text-sunyellow flex items-center gap-2 font-comic font-black text-base tracking-wider"> | |
| <span class="w-3 h-3 rounded-full bg-sunyellow animate-ping border border-black"></span> | |
| <span>🌊 TELEMETRY CONSOLE 📡</span> | |
| </span> | |
| <div class="flex items-center gap-2 font-mono text-xs bg-black px-3 py-1.5 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <span class="text-white">RAG: <strong id="ragLatencyDisplay" class="text-sunyellow font-bold">--</strong></span> | |
| <span class="text-slate-400 font-bold">|</span> | |
| <span class="text-white">TOTAL: <strong id="totalLatencyDisplay" class="text-surfpink font-bold">--</strong></span> | |
| </div> | |
| </div> | |
| <!-- 5 Stage Latencies (100% Solid Opaque Black Cards) --> | |
| <div class="grid grid-cols-5 gap-2 text-center font-mono py-1"> | |
| <div class="bg-black p-2 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <div class="text-[8px] text-sunyellow font-bold uppercase">STT (VOICE)</div> | |
| <div id="sttLatencyVal" class="text-xs font-bold text-white mt-0.5">--</div> | |
| </div> | |
| <div class="bg-black p-2 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <div class="text-[8px] text-purple-300 font-bold uppercase">EMBEDDING</div> | |
| <div id="embeddingLatencyVal" class="text-xs font-bold text-purple-300 mt-0.5">--</div> | |
| </div> | |
| <div class="bg-black p-2 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <div class="text-[8px] text-emerald-400 font-bold uppercase">RETRIEVAL</div> | |
| <div id="retrievalLatencyVal" class="text-xs font-bold text-emerald-300 mt-0.5">--</div> | |
| </div> | |
| <div class="bg-black p-2 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <div class="text-[8px] text-sky-400 font-bold uppercase">GUARDRAIL</div> | |
| <div id="guardrailLatencyVal" class="text-xs font-bold text-sky-300 mt-0.5">--</div> | |
| </div> | |
| <div class="bg-black p-2 border-2 border-sunyellow rounded-xl shadow-brutal-sm"> | |
| <div class="text-[8px] text-surfpink font-bold uppercase">GENERATION</div> | |
| <div id="genLatencyVal" class="text-xs font-bold text-surfpink mt-0.5">--</div> | |
| </div> | |
| </div> | |
| <!-- Visual Split Progress Bar (100% Solid Opaque) --> | |
| <div class="flex flex-col gap-1.5 font-mono text-[10px] bg-black p-3 rounded-xl border-2 border-sunyellow shadow-brutal-sm"> | |
| <div class="flex items-center justify-between text-white text-[9px] flex-wrap gap-1"> | |
| <span class="font-bold text-sunyellow">OCEAN LATENCY SPLIT</span> | |
| <div class="flex items-center gap-1.5 flex-wrap"> | |
| <span id="splitLabelSTT" class="text-sunyellow">STT: --</span> | |
| <span id="splitLabelEmbed" class="text-purple-300">EMB: --</span> | |
| <span id="splitLabelRetrieval" class="text-emerald-400">RETR: --</span> | |
| <span id="splitLabelGuardrail" class="text-sky-400">GUARD: --</span> | |
| <span id="splitLabelGen" class="text-surfpink">GEN: --</span> | |
| </div> | |
| </div> | |
| <div class="w-full h-3.5 bg-jungle-950 rounded-full overflow-hidden flex border-2 border-sunyellow"> | |
| <div id="splitBarSTT" class="h-full bg-sunyellow transition-all duration-300" style="width: 0%"></div> | |
| <div id="splitBarEmbed" class="h-full bg-purple-400 transition-all duration-300" style="width: 0%"></div> | |
| <div id="splitBarRetrieval" class="h-full bg-emerald-400 transition-all duration-300" style="width: 0%"></div> | |
| <div id="splitBarGuardrail" class="h-full bg-sky-400 transition-all duration-300" style="width: 0%"></div> | |
| <div id="splitBarGen" class="h-full bg-surfpink transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Expandable Waterfall Breakdown Toggle --> | |
| <div class="border-t-2 border-sunyellow pt-2 font-mono"> | |
| <button id="toggleWaterfallBtn" type="button" class="w-full text-left flex items-center justify-between text-[11px] text-sunyellow hover:text-white py-1 transition font-bold"> | |
| <span class="flex items-center gap-1 font-bold"> | |
| <span class="material-symbols-outlined text-xs text-sunyellow">waterfall_chart</span> | |
| <span>DETAILED STAGE TIMINGS (9 STAGES)</span> | |
| </span> | |
| <span id="waterfallToggleIcon" class="material-symbols-outlined text-xs">expand_more</span> | |
| </button> | |
| <div id="stageWaterfallList" class="hidden mt-2 pt-2 border-t-2 border-sunyellow flex flex-col gap-1 text-[10px] bg-black p-3 rounded-xl border-2 border-sunyellow"> | |
| <div class="text-slate-300 text-[10px] text-center italic">Run a query to inspect stage timings</div> | |
| </div> | |
| </div> | |
| <!-- Benchmark Quantiles in Neon Pink --> | |
| <div class="grid grid-cols-3 gap-2 text-center font-mono pt-2 border-t border-cyan-500/30"> | |
| <div> | |
| <div class="text-[9px] text-slate-400">P50 (TARGET <200ms)</div> | |
| <div class="text-xs font-bold text-beachpink mt-0.5">38.99ms</div> | |
| </div> | |
| <div> | |
| <div class="text-[9px] text-slate-400">P70</div> | |
| <div class="text-xs font-bold text-beachpink mt-0.5">41.33ms</div> | |
| </div> | |
| <div> | |
| <div class="text-[9px] text-slate-400">P100</div> | |
| <div class="text-xs font-bold text-beachpink mt-0.5">52.30ms</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: GUARDRAIL AUDIT MATRIX (SURFBOARD SHIELD SHAPE & BEACH SAND THEME) --> | |
| <div class="lg:col-span-6 shape-guardrail-matrix beach-sand-bg p-5 flex flex-col justify-between gap-4 relative z-20 bg-[#FFFBEA] opacity-100"> | |
| <!-- Header --> | |
| <div class="flex items-center justify-between border-b-3 border-black pb-2.5"> | |
| <h4 class="font-comic font-black text-lg md:text-xl text-black tracking-wider flex items-center gap-2"> | |
| <span>🛡️ GUARDRAIL AUDIT MATRIX 🏄♂️</span> | |
| </h4> | |
| <span class="comic-badge px-2.5 py-0.5 bg-black text-beachgreen text-xs shadow-brutal-sm">4-TIER ACTIVE</span> | |
| </div> | |
| <!-- 2x2 Matrix Cards (Beach Shell & Surfboard Pop Art Cards) --> | |
| <div class="grid grid-cols-2 gap-3 font-comic text-xs"> | |
| <div class="p-3 bg-white border-3 border-black rounded-2xl shadow-brutal-sm flex flex-col justify-between gap-2 hover-pop"> | |
| <div class="flex items-center justify-between text-slate-800 font-bold"> | |
| <span class="flex items-center gap-1 text-black font-black"> | |
| <span>⚓</span> GROUNDING | |
| </span> | |
| <span id="badgeGrounding" class="comic-badge px-2 py-0.5 bg-black text-beachgold text-[10px]">STANDBY</span> | |
| </div> | |
| <p class="text-[10px] text-slate-600 leading-tight">Verifies response alignment with vector passages.</p> | |
| </div> | |
| <div class="p-3 bg-white border-3 border-black rounded-2xl shadow-brutal-sm flex flex-col justify-between gap-2 hover-pop"> | |
| <div class="flex items-center justify-between text-slate-800 font-bold"> | |
| <span class="flex items-center gap-1 text-black font-black"> | |
| <span>🌴</span> TOPIC | |
| </span> | |
| <span id="badgeTopic" class="comic-badge px-2 py-0.5 bg-black text-beachcyan text-[10px]">STANDBY</span> | |
| </div> | |
| <p class="text-[10px] text-slate-600 leading-tight">Screens domain query intent & relevance taxonomy.</p> | |
| </div> | |
| <div class="p-3 bg-white border-3 border-black rounded-2xl shadow-brutal-sm flex flex-col justify-between gap-2 hover-pop"> | |
| <div class="flex items-center justify-between text-slate-800 font-bold"> | |
| <span class="flex items-center gap-1 text-black font-black"> | |
| <span>☀️</span> HALLUCINATION | |
| </span> | |
| <span id="badgeHallucination" class="comic-badge px-2 py-0.5 bg-black text-beachpink text-[10px]">STANDBY</span> | |
| </div> | |
| <p class="text-[10px] text-slate-600 leading-tight">Filters ungrounded claims before LLM synthesis.</p> | |
| </div> | |
| <div class="p-3 bg-white border-3 border-black rounded-2xl shadow-brutal-sm flex flex-col justify-between gap-2 hover-pop"> | |
| <div class="flex items-center justify-between text-slate-800 font-bold"> | |
| <span class="flex items-center gap-1 text-black font-black"> | |
| <span>🏄</span> SAFETY | |
| </span> | |
| <span id="badgeSafety" class="comic-badge px-2 py-0.5 bg-black text-beachgreen text-[10px]">SECURE ✓</span> | |
| </div> | |
| <p class="text-[10px] text-slate-600 leading-tight">Neural Prompt-Guard ONNX shield active.</p> | |
| </div> | |
| </div> | |
| <!-- Policy Footnote --> | |
| <div class="bg-white border-2 border-black p-2.5 rounded-xl shadow-brutal-sm flex items-start gap-2"> | |
| <span class="material-symbols-outlined text-beachpink text-base font-black">verified_user</span> | |
| <p class="text-[10px] font-comicbody font-bold text-slate-800 leading-snug"> | |
| The RAG pipeline knows when NOT to answer. Insufficient context triggers a graceful grounding notice rather than hallucination. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- CLIENT JAVASCRIPT FOR INTERACTION & API INTEGRATION --> | |
| <script> | |
| let currentLanguage = "en"; | |
| let isRecording = false; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let audioContext = null; | |
| let analyser = null; | |
| let dataArray = null; | |
| // DOM Elements | |
| const recordBtn = document.getElementById("recordBtn"); | |
| const micIcon = document.getElementById("micIcon"); | |
| const sttStatusBadge = document.getElementById("sttStatusBadge"); | |
| const sttStatusText = document.getElementById("sttStatusText"); | |
| const queryInput = document.getElementById("queryInput"); | |
| const searchBtn = document.getElementById("searchBtn"); | |
| const fieldNoteCard = document.getElementById("fieldNoteCard"); | |
| const fieldNoteText = document.getElementById("fieldNoteText"); | |
| const passagesList = document.getElementById("passagesList"); | |
| const topKBadge = document.getElementById("topKBadge"); | |
| const answerContent = document.getElementById("answerContent"); | |
| const answerSourceBadge = document.getElementById("answerSourceBadge"); | |
| const answerStatusTag = document.getElementById("answerStatusTag"); | |
| const answerStatusIcon = document.getElementById("answerStatusIcon"); | |
| const retrievalTimePill = document.getElementById("retrievalTimePill"); | |
| const detectedLangPill = document.getElementById("detectedLangPill"); | |
| const crossLingualToggle = document.getElementById("crossLingualToggle"); | |
| // Telemetry DOM | |
| const ragLatencyDisplay = document.getElementById("ragLatencyDisplay"); | |
| const totalLatencyDisplay = document.getElementById("totalLatencyDisplay"); | |
| const sttLatencyVal = document.getElementById("sttLatencyVal"); | |
| const embeddingLatencyVal = document.getElementById("embeddingLatencyVal"); | |
| const retrievalLatencyVal = document.getElementById("retrievalLatencyVal"); | |
| const guardrailLatencyVal = document.getElementById("guardrailLatencyVal"); | |
| const genLatencyVal = document.getElementById("genLatencyVal"); | |
| // Guardrail DOM | |
| const badgeGrounding = document.getElementById("badgeGrounding"); | |
| const badgeTopic = document.getElementById("badgeTopic"); | |
| const badgeHallucination = document.getElementById("badgeHallucination"); | |
| const badgeSafety = document.getElementById("badgeSafety"); | |
| // Waterfall Toggle Button | |
| const toggleWaterfallBtn = document.getElementById("toggleWaterfallBtn"); | |
| const stageWaterfallList = document.getElementById("stageWaterfallList"); | |
| const waterfallToggleIcon = document.getElementById("waterfallToggleIcon"); | |
| if (toggleWaterfallBtn && stageWaterfallList) { | |
| toggleWaterfallBtn.addEventListener("click", () => { | |
| const isHidden = stageWaterfallList.classList.contains("hidden"); | |
| if (isHidden) { | |
| stageWaterfallList.classList.remove("hidden"); | |
| if (waterfallToggleIcon) waterfallToggleIcon.textContent = "expand_less"; | |
| } else { | |
| stageWaterfallList.classList.add("hidden"); | |
| if (waterfallToggleIcon) waterfallToggleIcon.textContent = "expand_more"; | |
| } | |
| }); | |
| } | |
| // Language Selector Buttons Sync | |
| document.querySelectorAll(".lang-btn").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| const lang = btn.getAttribute("data-lang"); | |
| currentLanguage = lang; | |
| document.querySelectorAll(".lang-btn").forEach(b => { | |
| if (b.getAttribute("data-lang") === lang) { | |
| b.classList.add("bg-black", "text-beachgold"); | |
| b.classList.remove("bg-white", "text-black"); | |
| } else { | |
| b.classList.remove("bg-black", "text-beachgold"); | |
| b.classList.add("bg-white", "text-black"); | |
| } | |
| }); | |
| }); | |
| }); | |
| // Quick Prompt Chips Click Handler | |
| document.querySelectorAll(".quick-chip").forEach(chip => { | |
| chip.addEventListener("click", () => { | |
| const text = chip.getAttribute("data-text"); | |
| const lang = chip.getAttribute("data-lang"); | |
| if (text) { | |
| queryInput.value = text; | |
| if (lang) { | |
| currentLanguage = lang; | |
| document.querySelectorAll(".lang-btn").forEach(b => { | |
| if (b.getAttribute("data-lang") === lang) { | |
| b.classList.add("bg-black", "text-hhgold"); | |
| b.classList.remove("text-slate-700"); | |
| } else { | |
| b.classList.remove("bg-black", "text-hhgold"); | |
| b.classList.add("text-slate-700"); | |
| } | |
| }); | |
| } | |
| executeSearch(); | |
| } | |
| }); | |
| }); | |
| // Enter Key Search | |
| queryInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter") executeSearch(); | |
| }); | |
| searchBtn.addEventListener("click", executeSearch); | |
| function getApiUrl(path) { | |
| if (window.API_BASE_URL) { | |
| return window.API_BASE_URL.replace(/\/+$/, "") + path; | |
| } | |
| const stored = localStorage.getItem("VECTOR_API_URL"); | |
| if (stored) { | |
| return stored.replace(/\/+$/, "") + path; | |
| } | |
| return path; | |
| } | |
| async function executeSearch() { | |
| const text = queryInput.value.trim(); | |
| if (!text) return; | |
| searchBtn.disabled = true; | |
| searchBtn.innerHTML = `<span class="material-symbols-outlined animate-spin text-sm">progress_activity</span><span>RUN</span>`; | |
| if (sttStatusText) sttStatusText.textContent = "SEARCHING KNOWLEDGE SEA..."; | |
| const formData = new FormData(); | |
| formData.append("text", text); | |
| formData.append("language_hint", currentLanguage); | |
| const isCrossLingual = crossLingualToggle?.checked ?? false; | |
| formData.append("cross_lingual", isCrossLingual ? "true" : "false"); | |
| fieldNoteCard.classList.remove("hidden"); | |
| fieldNoteText.textContent = `"${text}"`; | |
| try { | |
| const response = await fetch(getApiUrl("/query"), { method: "POST", body: formData }); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| let msg = response.statusText || ("HTTP " + response.status); | |
| try { | |
| const parsed = JSON.parse(errText); | |
| msg = parsed.detail || parsed.reason || msg; | |
| } catch (_) { | |
| if (errText) msg = errText; | |
| } | |
| alert("Query Error (" + response.status + "): " + msg); | |
| return; | |
| } | |
| const data = await response.json(); | |
| renderResults(data); | |
| } catch (err) { | |
| alert("Query error: " + err); | |
| } finally { | |
| searchBtn.disabled = false; | |
| searchBtn.innerHTML = `<span>RUN</span><span class="material-symbols-outlined text-sm font-bold">arrow_forward</span>`; | |
| if (sttStatusText) sttStatusText.textContent = "RESPONSE DELIVERED"; | |
| } | |
| } | |
| // Voice Recording Integration | |
| recordBtn.addEventListener("click", async () => { | |
| if (!isRecording) { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| const source = audioContext.createMediaStreamSource(stream); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 64; | |
| source.connect(analyser); | |
| dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = e => audioChunks.push(e.data); | |
| mediaRecorder.onstop = async () => { | |
| const audioBlob = new Blob(audioChunks, { type: "audio/wav" }); | |
| sendVoiceQuery(audioBlob); | |
| if (audioContext) audioContext.close(); | |
| }; | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| micIcon.classList.remove("text-black", "hover:text-slate-800"); | |
| micIcon.classList.add("text-red-600", "animate-pulse"); | |
| if (sttStatusText) sttStatusText.textContent = "SARVAM STT LISTENING..."; | |
| micIcon.textContent = "stop"; | |
| } catch (err) { | |
| alert("Microphone access denied: " + err); | |
| } | |
| } else { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| micIcon.classList.remove("text-red-600", "animate-pulse"); | |
| micIcon.classList.add("text-black", "hover:text-slate-800"); | |
| if (sttStatusText) sttStatusText.textContent = "SARVAM STT TRANSCRIBING..."; | |
| micIcon.textContent = "mic"; | |
| } | |
| }); | |
| async function sendVoiceQuery(blob) { | |
| searchBtn.disabled = true; | |
| recordBtn.disabled = true; | |
| const formData = new FormData(); | |
| formData.append("file", blob, "voice_input.wav"); | |
| formData.append("language_hint", currentLanguage); | |
| const isCrossLingual = crossLingualToggle?.checked ?? false; | |
| formData.append("cross_lingual", isCrossLingual ? "true" : "false"); | |
| try { | |
| const response = await fetch(getApiUrl("/query"), { method: "POST", body: formData }); | |
| if (!response.ok) { | |
| const errText = await response.text(); | |
| let msg = response.statusText || ("HTTP " + response.status); | |
| try { | |
| const parsed = JSON.parse(errText); | |
| msg = parsed.detail || parsed.reason || msg; | |
| } catch (_) { | |
| if (errText) msg = errText; | |
| } | |
| alert("Voice Query Error (" + response.status + "): " + msg); | |
| return; | |
| } | |
| const data = await response.json(); | |
| renderResults(data); | |
| } catch (err) { | |
| alert("Voice query error: " + err); | |
| } finally { | |
| searchBtn.disabled = false; | |
| recordBtn.disabled = false; | |
| if (sttStatusText) sttStatusText.textContent = "RESPONSE DELIVERED"; | |
| } | |
| } | |
| function renderResults(data) { | |
| // 0. Update Field Note | |
| fieldNoteCard.classList.remove("hidden"); | |
| if (data.transcript) { | |
| fieldNoteText.textContent = `"${data.transcript}"`; | |
| queryInput.value = data.transcript; | |
| } else if (data.query) { | |
| fieldNoteText.textContent = `"${data.query}"`; | |
| } | |
| // Routed Language Pill | |
| if (data.language_detected) { | |
| detectedLangPill.classList.remove("hidden"); | |
| detectedLangPill.textContent = `ROUTED: ${data.language_detected.toUpperCase()}`; | |
| } | |
| // 1. Answer Card Styling | |
| const src = (data.answer_source || "declined").toLowerCase(); | |
| const flags = data.guardrail_flags || {}; | |
| retrievalTimePill.textContent = `LATENCY: ${data.retrieval_ms.toFixed(1)}ms`; | |
| if (src === "cross_lingual_synthesis") { | |
| answerSourceBadge.textContent = "CROSS-LINGUAL FEDERATION"; | |
| answerStatusTag.textContent = "GROUNDED ANSWER GENERATED"; | |
| answerStatusTag.className = "text-hhdarkgreen tracking-wider uppercase font-bold"; | |
| answerStatusIcon.textContent = "verified"; | |
| answerStatusIcon.className = "material-symbols-outlined text-hhdarkgreen text-base font-bold"; | |
| answerContent.className = "font-serif text-base md:text-lg text-black leading-relaxed min-h-[50px]"; | |
| answerContent.textContent = data.answer || "No grounded answer available."; | |
| } else if (src === "extractive" || src === "generated" || src === "local_slm_generated") { | |
| answerSourceBadge.textContent = src === "extractive" ? "EXTRACTIVE (EXACT)" : (src === "local_slm_generated" ? "OFFLINE SLM" : "GROUNDED GENERATION"); | |
| answerStatusTag.textContent = "GROUNDED ANSWER GENERATED"; | |
| answerStatusTag.className = "text-hhdarkgreen tracking-wider uppercase font-bold"; | |
| answerStatusIcon.textContent = "verified"; | |
| answerStatusIcon.className = "material-symbols-outlined text-hhdarkgreen text-base font-bold"; | |
| answerContent.className = "font-serif text-base md:text-lg text-black leading-relaxed min-h-[50px]"; | |
| answerContent.textContent = data.answer || "No grounded answer available."; | |
| } else if (src.includes("cache")) { | |
| answerSourceBadge.textContent = "SEMANTIC CACHE"; | |
| answerStatusTag.textContent = "VERIFIED CACHED GROUNDING"; | |
| answerStatusTag.className = "text-hhdarkgreen tracking-wider uppercase font-bold"; | |
| answerStatusIcon.textContent = "bolt"; | |
| answerStatusIcon.className = "material-symbols-outlined text-hhdarkgreen text-base font-bold"; | |
| answerContent.className = "font-serif text-base md:text-lg text-black leading-relaxed min-h-[50px]"; | |
| answerContent.textContent = data.answer || "No grounded answer available."; | |
| } else if (flags.unsafe_detected) { | |
| answerSourceBadge.textContent = "SAFETY BLOCKED"; | |
| answerStatusTag.textContent = "SAFETY INTERVENTION: PROMPT BLOCKED"; | |
| answerStatusTag.className = "text-red-700 tracking-wider uppercase font-black"; | |
| answerStatusIcon.textContent = "block"; | |
| answerStatusIcon.className = "material-symbols-outlined text-red-600 text-base font-bold"; | |
| answerContent.className = "font-serif text-sm text-red-900 leading-relaxed min-h-[50px] bg-red-100 p-3 border-2 border-red-500"; | |
| answerContent.textContent = data.answer || "Blocked by safety guardrail."; | |
| } else { | |
| answerSourceBadge.textContent = "INSUFFICIENT CONTEXT"; | |
| answerStatusTag.textContent = "GROUNDING NOTICE: INSUFFICIENT EVIDENCE"; | |
| answerStatusTag.className = "text-amber-800 tracking-wider uppercase font-bold"; | |
| answerStatusIcon.textContent = "info"; | |
| answerStatusIcon.className = "material-symbols-outlined text-amber-700 text-base font-bold"; | |
| answerContent.className = "font-serif text-sm text-slate-800 leading-relaxed min-h-[50px] bg-amber-50 p-3 border-2 border-amber-400"; | |
| answerContent.textContent = data.answer || "The indexed corpus does not contain enough grounded facts to answer this question."; | |
| } | |
| // 2. Knowledge Sea Passages Stream | |
| const chunks = data.retrieved_chunks || []; | |
| topKBadge.textContent = `TOP-K: ${chunks.length} PASSAGES`; | |
| passagesList.innerHTML = ""; | |
| if (chunks.length === 0) { | |
| passagesList.innerHTML = `<div class="p-4 bg-sand-100 border-2 border-black text-center font-mono text-xs text-slate-600">No context passages retrieved in indexed corpus.</div>`; | |
| } else { | |
| chunks.forEach((c, idx) => { | |
| const item = document.createElement("div"); | |
| item.className = "p-3.5 bg-sand-100 border-2 border-black shadow-brutal relative overflow-hidden flex flex-col gap-1.5"; | |
| const langMap = { | |
| en: "bg-sky-900 text-sky-200", | |
| hi: "bg-amber-900 text-amber-200", | |
| mr: "bg-red-900 text-red-200" | |
| }; | |
| const lang = (c.source_lang || "").toLowerCase(); | |
| let langPillBg = langMap[lang] || "bg-slate-800 text-slate-200"; | |
| item.innerHTML = ` | |
| <div class="flex items-center justify-between mb-1"> | |
| <div class="flex items-center gap-1.5 font-mono text-xs font-bold"> | |
| <span class="material-symbols-outlined text-sm text-hhdarkgreen">database</span> | |
| <span>DOC_REF: ${(c.chunk_id || 'DOC_' + (idx+1)).toUpperCase()}</span> | |
| </div> | |
| <span class="px-2 py-0.5 bg-hhgold border border-black font-mono text-[10px] font-black uppercase">${(c.final_score * 100).toFixed(0)}% MATCH</span> | |
| </div> | |
| <p class="font-serif text-xs md:text-sm text-black leading-relaxed">"${c.text}"</p> | |
| <div class="mt-1 flex items-center justify-between text-[10px] font-mono text-slate-600 border-t border-slate-300 pt-1.5"> | |
| <span class="flex items-center gap-1.5"> | |
| <span class="px-1.5 py-0.5 ${langPillBg} rounded font-bold uppercase">${c.source_lang.toUpperCase()}</span> | |
| <span>${c.chunk_strategy.replace(/_/g, ' ').toUpperCase()}</span> | |
| </span> | |
| <span>${c.bm25_score !== null && c.bm25_score !== undefined ? 'BM25: ' + c.bm25_score.toFixed(2) : 'DENSE: ' + c.dense_score.toFixed(3)}</span> | |
| </div> | |
| `; | |
| passagesList.appendChild(item); | |
| }); | |
| } | |
| // Helper function for sub-millisecond precision formatting | |
| function formatLatency(ms) { | |
| if (ms === undefined || ms === null || isNaN(ms)) return "--"; | |
| if (ms === 0) return "0.0ms"; | |
| if (ms < 0.01) return "<0.01ms"; | |
| if (ms < 1.0) return `${ms.toFixed(2)}ms`; | |
| if (ms < 10.0) return `${ms.toFixed(2)}ms`; | |
| return `${ms.toFixed(1)}ms`; | |
| } | |
| // 3. Telemetry Console Metrics & Split Breakdown (5 Distinct Stages) | |
| const timings = data.stage_timings || []; | |
| let sttMs = 0; | |
| let embedMs = 0; | |
| let retrievalMs = 0; | |
| let guardrailMs = 0; | |
| let genMs = 0; | |
| timings.forEach(t => { | |
| const stage = t.stage.toLowerCase(); | |
| if (stage.includes("stt")) { | |
| sttMs += t.ms; | |
| } else if (stage.includes("embedding")) { | |
| embedMs += t.ms; | |
| } else if (stage.includes("retrieval") || stage.includes("rerank") || stage.includes("bm25") || stage.includes("cache")) { | |
| retrievalMs += t.ms; | |
| } else if (stage.includes("guardrail") || stage.includes("safety") || stage.includes("topic") || stage.includes("grounding") || stage.includes("routing")) { | |
| guardrailMs += t.ms; | |
| } else if (stage.includes("generation") || stage.includes("synthesis")) { | |
| genMs += t.ms; | |
| } else { | |
| retrievalMs += t.ms; | |
| } | |
| }); | |
| // Update UI 5-Stage Metric Boxes | |
| sttLatencyVal.textContent = sttMs > 0 ? formatLatency(sttMs) : "0.0ms (Text)"; | |
| embeddingLatencyVal.textContent = formatLatency(embedMs); | |
| retrievalLatencyVal.textContent = formatLatency(retrievalMs); | |
| guardrailLatencyVal.textContent = formatLatency(guardrailMs); | |
| genLatencyVal.textContent = formatLatency(genMs); | |
| const ragMs = embedMs + retrievalMs + guardrailMs + genMs; | |
| const totalMs = data.total_ms || (sttMs + ragMs); | |
| if (ragLatencyDisplay) ragLatencyDisplay.textContent = formatLatency(ragMs); | |
| if (totalLatencyDisplay) totalLatencyDisplay.textContent = formatLatency(totalMs); | |
| // Calculate Proportional Split Percentages that exactly sum to 100% | |
| const totalSum = Math.max(0.001, sttMs + embedMs + retrievalMs + guardrailMs + genMs); | |
| const sttPct = ((sttMs / totalSum) * 100).toFixed(1); | |
| const embedPct = ((embedMs / totalSum) * 100).toFixed(1); | |
| const retrPct = ((retrievalMs / totalSum) * 100).toFixed(1); | |
| const guardPct = ((guardrailMs / totalSum) * 100).toFixed(1); | |
| const genPct = ((genMs / totalSum) * 100).toFixed(1); | |
| // Update Visual Split Progress Bar | |
| document.getElementById("splitBarSTT").style.width = `${Math.max(sttMs > 0 ? 3 : 0, (sttMs / totalSum) * 100)}%`; | |
| document.getElementById("splitBarEmbed").style.width = `${Math.max(embedMs > 0 ? 3 : 0, (embedMs / totalSum) * 100)}%`; | |
| document.getElementById("splitBarRetrieval").style.width = `${Math.max(retrievalMs > 0 ? 3 : 0, (retrievalMs / totalSum) * 100)}%`; | |
| document.getElementById("splitBarGuardrail").style.width = `${Math.max(guardrailMs > 0 ? 2 : 0, (guardrailMs / totalSum) * 100)}%`; | |
| document.getElementById("splitBarGen").style.width = `${Math.max(genMs > 0 ? 2 : 0, (genMs / totalSum) * 100)}%`; | |
| document.getElementById("splitLabelSTT").textContent = `STT: ${sttPct}%`; | |
| document.getElementById("splitLabelEmbed").textContent = `EMB: ${embedPct}%`; | |
| document.getElementById("splitLabelRetrieval").textContent = `RETR: ${retrPct}%`; | |
| document.getElementById("splitLabelGuardrail").textContent = `GUARD: ${guardPct}%`; | |
| document.getElementById("splitLabelGen").textContent = `GEN: ${genPct}%`; | |
| // Populate Detailed 9-Stage Waterfall List | |
| const waterfallList = document.getElementById("stageWaterfallList"); | |
| if (waterfallList && timings.length > 0) { | |
| waterfallList.innerHTML = ""; | |
| timings.forEach((t, i) => { | |
| const row = document.createElement("div"); | |
| row.className = "flex items-center justify-between py-1 border-b border-emerald-950/40"; | |
| const stageName = t.stage.replace(/_/g, ' ').toUpperCase(); | |
| const fallbackBadge = t.fallback_used ? '<span class="text-amber-400 text-[8px] ml-1">[FALLBACK]</span>' : ''; | |
| row.innerHTML = ` | |
| <div class="flex items-center gap-1.5 truncate max-w-[220px]"> | |
| <span class="text-emerald-500 font-bold">${i+1}.</span> | |
| <span class="text-slate-300 truncate">${stageName}</span> | |
| ${fallbackBadge} | |
| </div> | |
| <span class="font-bold text-hhemerald ml-2">${formatLatency(t.ms)}</span> | |
| `; | |
| waterfallList.appendChild(row); | |
| }); | |
| } | |
| // 4. Guardrail Status Badges | |
| if (flags.grounding_passed) { | |
| badgeGrounding.textContent = "VERIFIED ✓"; | |
| badgeGrounding.className = "font-bold text-emerald-700"; | |
| } else { | |
| badgeGrounding.textContent = "UNGROUNDED ✗"; | |
| badgeGrounding.className = "font-bold text-amber-700"; | |
| } | |
| if (!flags.off_topic_detected) { | |
| badgeTopic.textContent = "ALIGNED ✓"; | |
| badgeTopic.className = "font-bold text-emerald-700"; | |
| } else { | |
| badgeTopic.textContent = "OFF-TOPIC ✗"; | |
| badgeTopic.className = "font-bold text-red-600"; | |
| } | |
| if (flags.grounding_passed && src !== "declined") { | |
| badgeHallucination.textContent = "UNDETECTED ✓"; | |
| badgeHallucination.className = "font-bold text-emerald-700"; | |
| } else { | |
| badgeHallucination.textContent = "FLAGGED ⚠"; | |
| badgeHallucination.className = "font-bold text-amber-700"; | |
| } | |
| if (!flags.unsafe_detected) { | |
| badgeSafety.textContent = "SECURE ✓"; | |
| badgeSafety.className = "font-bold text-emerald-700"; | |
| } else { | |
| badgeSafety.textContent = "VIOLATION ✗"; | |
| badgeSafety.className = "font-bold text-red-600"; | |
| } | |
| } | |
| // Spider Web Canvas Visualizer & Audio Reactivity | |
| const canvas = document.getElementById("waveformCanvas"); | |
| const ctx = canvas.getContext("2d"); | |
| function resizeCanvas() { | |
| if (canvas && canvas.parentElement) { | |
| canvas.width = canvas.parentElement.clientWidth; | |
| canvas.height = canvas.parentElement.clientHeight; | |
| } | |
| } | |
| window.addEventListener("resize", resizeCanvas); | |
| resizeCanvas(); | |
| let wavePhase = 0; | |
| function drawSpiderWeb() { | |
| if (!canvas || !ctx) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| wavePhase += 0.035; | |
| const cx = canvas.width / 2; | |
| const cy = canvas.height / 2; | |
| const maxR = Math.min(cx, cy) * 0.96; | |
| const numSpokes = 12; | |
| const numRings = 5; | |
| // 1. Fill Dark Forest Green (#053b22) Mask Background | |
| ctx.fillStyle = "#053b22"; | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, maxR, 0, Math.PI * 2); | |
| ctx.fill(); | |
| // Audio reactivity amplitude calculation | |
| let audioGain = 0; | |
| if (analyser && dataArray && isRecording) { | |
| analyser.getByteFrequencyData(dataArray); | |
| let sum = 0; | |
| for (let i = 0; i < dataArray.length; i++) sum += dataArray[i]; | |
| audioGain = (sum / dataArray.length) / 255; | |
| } | |
| const greenStroke = isRecording ? "#00ffaa" : "#00ff66"; | |
| // 2. Draw Spider Web Radial Spokes (Green lines originating from central nose bridge) | |
| ctx.strokeStyle = greenStroke; | |
| ctx.lineWidth = 2.2; | |
| for (let i = 0; i < numSpokes; i++) { | |
| const angle = (i * Math.PI * 2) / numSpokes - Math.PI / 2; | |
| const x = cx + Math.cos(angle) * maxR; | |
| const y = cy + Math.sin(angle) * maxR; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy + 10); | |
| ctx.lineTo(x, y); | |
| ctx.stroke(); | |
| } | |
| // 3. Draw Concentric Spider Web Arcs | |
| for (let r = 1; r <= numRings; r++) { | |
| const ringFraction = r / numRings; | |
| const baseRadius = maxR * ringFraction + (audioGain * 14 * Math.sin(wavePhase + r)); | |
| ctx.strokeStyle = isRecording | |
| ? `rgba(0, 255, 170, ${0.7 + r * 0.05})` | |
| : `rgba(0, 255, 102, ${0.75 + r * 0.04})`; | |
| ctx.lineWidth = isRecording ? 2.6 : 2.0; | |
| const ringPoints = []; | |
| for (let i = 0; i < numSpokes; i++) { | |
| const angle = (i * Math.PI * 2) / numSpokes - Math.PI / 2; | |
| const px = cx + Math.cos(angle) * baseRadius; | |
| const py = cy + 10 + Math.sin(angle) * baseRadius; | |
| ringPoints.push({ x: px, y: py, angle: angle }); | |
| } | |
| // Draw dipping spider web arcs | |
| for (let i = 0; i < numSpokes; i++) { | |
| const p1 = ringPoints[i]; | |
| const p2 = ringPoints[(i + 1) % numSpokes]; | |
| const midAngle = (p1.angle + p2.angle) / 2 + (i === numSpokes - 1 ? Math.PI : 0); | |
| const dipRadius = baseRadius * 0.85; | |
| const midX = cx + Math.cos(midAngle) * dipRadius; | |
| const midY = cy + 10 + Math.sin(midAngle) * dipRadius; | |
| ctx.beginPath(); | |
| ctx.moveTo(p1.x, p1.y); | |
| ctx.quadraticCurveTo(midX, midY, p2.x, p2.y); | |
| ctx.stroke(); | |
| } | |
| } | |
| // 4. Draw Thick Outer Green Spider-Man Circle Border | |
| ctx.strokeStyle = greenStroke; | |
| ctx.lineWidth = 6.5; | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, maxR, 0, Math.PI * 2); | |
| ctx.stroke(); | |
| // 5. Draw Iconic Spider-Man Angled Eyes (Symmetrical Left & Right with Thick Green Borders) | |
| function drawSpiderEye(isLeft) { | |
| const s = isLeft ? -1 : 1; | |
| ctx.save(); | |
| ctx.beginPath(); | |
| // Eye anchor coordinates matching iconic Spider-Man mask geometry | |
| const topInnerX = cx + s * (maxR * 0.08); | |
| const topInnerY = cy - (maxR * 0.04); | |
| const outerPointX = cx + s * (maxR * 0.72); | |
| const outerPointY = cy - (maxR * 0.28); | |
| const outerBottomX = cx + s * (maxR * 0.52); | |
| const outerBottomY = cy + (maxR * 0.42); | |
| ctx.moveTo(topInnerX, topInnerY); | |
| // Slanted top edge outwards to outer sharp corner | |
| ctx.quadraticCurveTo(cx + s * (maxR * 0.38), cy - (maxR * 0.34), outerPointX, outerPointY); | |
| // Outer curve going down to bottom corner | |
| ctx.quadraticCurveTo(cx + s * (maxR * 0.78), cy + (maxR * 0.08), outerBottomX, outerBottomY); | |
| // Smooth bottom curve returning up to inner point | |
| ctx.quadraticCurveTo(cx + s * (maxR * 0.22), cy + (maxR * 0.44), topInnerX, topInnerY); | |
| ctx.closePath(); | |
| // Fill Eye with Solid Bright White | |
| ctx.fillStyle = "#ffffff"; | |
| ctx.fill(); | |
| // Thick Bold Black Eye Frame Stroke Outline | |
| ctx.strokeStyle = "#000000"; | |
| ctx.lineWidth = 7.5; | |
| ctx.stroke(); | |
| ctx.restore(); | |
| } | |
| drawSpiderEye(true); // Left Spider Eye | |
| drawSpiderEye(false); // Right Spider Eye | |
| requestAnimationFrame(drawSpiderWeb); | |
| } | |
| drawSpiderWeb(); | |
| // ========================================== | |
| // MAIN PAGE FULL-SCREEN IRON MAN FLIGHT ENGINE | |
| // ========================================== | |
| const mainCanvas = document.getElementById("ironmanMainCanvas"); | |
| const mainCtx = mainCanvas ? mainCanvas.getContext("2d") : null; | |
| function resizeMainCanvas() { | |
| if (mainCanvas) { | |
| mainCanvas.width = window.innerWidth; | |
| mainCanvas.height = window.innerHeight; | |
| } | |
| } | |
| window.addEventListener("resize", resizeMainCanvas); | |
| resizeMainCanvas(); | |
| const mainIronmanImg = new Image(); | |
| mainIronmanImg.src = "/ironman.png"; | |
| let mainIronmanLoaded = false; | |
| mainIronmanImg.onload = () => { mainIronmanLoaded = true; }; | |
| let mainImX = -220; | |
| const mainThrusterParticles = []; | |
| function drawMainPageIronMan() { | |
| if (!mainCanvas || !mainCtx) return; | |
| const w = mainCanvas.width; | |
| const h = mainCanvas.height; | |
| mainCtx.clearRect(0, 0, w, h); | |
| // High-Speed Avengers Triangle / V-Formation Motion Engine (Speed = 5.6px/frame) | |
| const flightSpeed = 5.6; | |
| mainImX += flightSpeed; | |
| if (mainImX > w + 450) mainImX = -450; | |
| // Base Straight Diagonal Trajectory for Formation Leader (Iron Man at point of Triangle) | |
| const progress = (mainImX + 450) / (w + 900); | |
| const baseLeaderY = (h * 0.82) - progress * (h * 0.68); | |
| const mainImY = baseLeaderY; | |
| const imW = 165; | |
| const imH = 245; | |
| const bootX = mainImX + imW * 0.16; | |
| const bootY = mainImY + imH * 0.86; | |
| const handX = mainImX + imW * 0.30; | |
| const handY = mainImY + imH * 0.48; | |
| let boostPulse = Math.sin(wavePhase * 8) * 6; | |
| if (analyser && dataArray && isRecording) { | |
| analyser.getByteFrequencyData(dataArray); | |
| let sum = 0; | |
| for (let i = 0; i < 8; i++) sum += dataArray[i]; | |
| boostPulse += (sum / 8) * 0.25; | |
| } | |
| // Helper for MCU booster flame on main page | |
| function drawMainBoosterFlame(x, y, angle, length, width) { | |
| mainCtx.save(); | |
| mainCtx.translate(x, y); | |
| mainCtx.rotate(angle); | |
| // Outer Sky Blue Plasma Booster Flame | |
| const flameGrad = mainCtx.createLinearGradient(0, 0, length, 0); | |
| flameGrad.addColorStop(0, "rgba(255, 255, 255, 0.98)"); | |
| flameGrad.addColorStop(0.2, "rgba(135, 206, 250, 0.95)"); | |
| flameGrad.addColorStop(0.5, "rgba(0, 191, 255, 0.9)"); | |
| flameGrad.addColorStop(0.8, "rgba(0, 119, 255, 0.65)"); | |
| flameGrad.addColorStop(1, "rgba(0, 80, 255, 0)"); | |
| mainCtx.beginPath(); | |
| mainCtx.moveTo(0, -width / 2); | |
| mainCtx.lineTo(length + boostPulse, 0); | |
| mainCtx.lineTo(0, width / 2); | |
| mainCtx.closePath(); | |
| mainCtx.fillStyle = flameGrad; | |
| mainCtx.fill(); | |
| // Inner Pure White & Ice Blue Core Jet | |
| const coreGrad = mainCtx.createLinearGradient(0, 0, length * 0.65, 0); | |
| coreGrad.addColorStop(0, "#ffffff"); | |
| coreGrad.addColorStop(0.5, "#e0f7ff"); | |
| coreGrad.addColorStop(0.85, "#00bfff"); | |
| coreGrad.addColorStop(1, "rgba(0, 191, 255, 0)"); | |
| mainCtx.beginPath(); | |
| mainCtx.moveTo(0, -width * 0.25); | |
| mainCtx.lineTo(length * 0.6 + boostPulse * 0.5, 0); | |
| mainCtx.lineTo(0, width * 0.25); | |
| mainCtx.closePath(); | |
| mainCtx.fillStyle = coreGrad; | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // Draw main booster flames under boots & hands | |
| drawMainBoosterFlame(bootX, bootY, Math.PI * 0.82, 65 + boostPulse, 22); | |
| drawMainBoosterFlame(handX, handY, Math.PI * 0.84, 45 + boostPulse, 16); | |
| // Spawn Sky Blue thruster spark particles on main page | |
| for (let p = 0; p < 5; p++) { | |
| const origin = Math.random() > 0.5 ? { x: bootX, y: bootY } : { x: handX, y: handY }; | |
| mainThrusterParticles.push({ | |
| x: origin.x, | |
| y: origin.y, | |
| vx: -(2.5 + Math.random() * 4.5), | |
| vy: 1.2 + (Math.random() - 0.5) * 3, | |
| radius: 2.0 + Math.random() * 3.5, | |
| color: Math.random() > 0.4 ? "#00bfff" : (Math.random() > 0.5 ? "#87cefa" : "#ffffff"), | |
| life: 1.0, | |
| decay: 0.02 + Math.random() * 0.03 | |
| }); | |
| } | |
| // Update & render main thruster particles | |
| for (let i = mainThrusterParticles.length - 1; i >= 0; i--) { | |
| const pt = mainThrusterParticles[i]; | |
| pt.x += pt.vx; | |
| pt.y += pt.vy; | |
| pt.life -= pt.decay; | |
| if (pt.life <= 0) { | |
| mainThrusterParticles.splice(i, 1); | |
| continue; | |
| } | |
| mainCtx.save(); | |
| mainCtx.globalAlpha = pt.life; | |
| mainCtx.fillStyle = pt.color; | |
| mainCtx.shadowColor = pt.color; | |
| mainCtx.shadowBlur = 10; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(pt.x, pt.y, pt.radius * pt.life, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // Draw Bright Glowing Iron Man sprite on main page | |
| if (mainIronmanLoaded) { | |
| mainCtx.save(); | |
| mainCtx.filter = "brightness(1.45) contrast(1.3) saturate(1.35) drop-shadow(0 0 22px rgba(0, 220, 255, 0.95))"; | |
| mainCtx.drawImage(mainIronmanImg, mainImX, mainImY, imW, imH); | |
| mainCtx.restore(); | |
| // Chest Arc Reactor Energy Glow Lens Flare | |
| const chestX = mainImX + imW * 0.84; | |
| const chestY = mainImY + imH * 0.35; | |
| mainCtx.save(); | |
| const arcGrad = mainCtx.createRadialGradient(chestX, chestY, 0, chestX, chestY, 16); | |
| arcGrad.addColorStop(0, "rgba(255, 255, 255, 1)"); | |
| arcGrad.addColorStop(0.4, "rgba(0, 240, 255, 0.9)"); | |
| arcGrad.addColorStop(1, "rgba(0, 240, 255, 0)"); | |
| mainCtx.fillStyle = arcGrad; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(chestX, chestY, 16, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // ========================================== | |
| // DRAW THOR FLYING UPWARD (Top Wing of Triangle Formation) | |
| // ========================================== | |
| const thorX = mainImX - 220; | |
| const thorY = baseLeaderY - 95; | |
| const thorW = 180; | |
| const thorH = 240; | |
| // Stormbreaker Axe & Body Anchor Points | |
| const axeX = thorX + thorW * 0.42; | |
| const axeY = thorY + thorH * 0.22; | |
| const headX = thorX + thorW * 0.65; | |
| const headY = thorY + thorH * 0.38; | |
| const footX = thorX + thorW * 0.45; | |
| const footY = thorY + thorH * 0.88; | |
| // 1. Draw Crackling Lightning Bolts from Stormbreaker | |
| const numBolts = (analyser && dataArray && isRecording) ? 8 : 5; | |
| for (let b = 0; b < numBolts; b++) { | |
| const targetAngle = Math.random() * Math.PI * 2; | |
| const boltLength = 30 + Math.random() * 60; | |
| const endX = axeX + Math.cos(targetAngle) * boltLength; | |
| const endY = axeY + Math.sin(targetAngle) * boltLength; | |
| drawLightningArc(mainCtx, axeX, axeY, endX, endY, 12); | |
| } | |
| // Lightning arcs crackling across armor & cape | |
| drawLightningArc(mainCtx, axeX, axeY, headX, headY, 10); | |
| drawLightningArc(mainCtx, axeX, axeY, footX, footY, 14); | |
| // 2. Spawn Electric Thunder Spark Particles | |
| for (let s = 0; s < 4; s++) { | |
| thorSparks.push({ | |
| x: axeX + (Math.random() - 0.5) * 30, | |
| y: axeY + (Math.random() - 0.5) * 30, | |
| vx: -(2.0 + Math.random() * 4.0), | |
| vy: (Math.random() - 0.5) * 3.5, | |
| radius: 1.5 + Math.random() * 3.0, | |
| color: Math.random() > 0.4 ? "#00e5ff" : "#ffffff", | |
| life: 1.0, | |
| decay: 0.03 + Math.random() * 0.04 | |
| }); | |
| } | |
| // Update & render Thor electric sparks | |
| for (let i = thorSparks.length - 1; i >= 0; i--) { | |
| const sp = thorSparks[i]; | |
| sp.x += sp.vx; | |
| sp.y += sp.vy; | |
| sp.life -= sp.decay; | |
| if (sp.life <= 0) { | |
| thorSparks.splice(i, 1); | |
| continue; | |
| } | |
| mainCtx.save(); | |
| mainCtx.globalAlpha = sp.life; | |
| mainCtx.fillStyle = sp.color; | |
| mainCtx.shadowColor = "#00e5ff"; | |
| mainCtx.shadowBlur = 10; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(sp.x, sp.y, sp.radius * sp.life, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // 3. Draw Bright Luminous Thor Sprite with Electric Aura | |
| if (thorLoaded) { | |
| mainCtx.save(); | |
| mainCtx.filter = "brightness(1.4) contrast(1.28) saturate(1.3) drop-shadow(0 0 24px rgba(0, 229, 255, 0.95))"; | |
| mainCtx.drawImage(thorImg, thorX, thorY, thorW, thorH); | |
| mainCtx.restore(); | |
| // Stormbreaker Radial Thunder Glow Flare | |
| mainCtx.save(); | |
| const stormGrad = mainCtx.createRadialGradient(axeX, axeY, 0, axeX, axeY, 28); | |
| stormGrad.addColorStop(0, "rgba(255, 255, 255, 1)"); | |
| stormGrad.addColorStop(0.35, "rgba(0, 229, 255, 0.9)"); | |
| stormGrad.addColorStop(1, "rgba(0, 229, 255, 0)"); | |
| mainCtx.fillStyle = stormGrad; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(axeX, axeY, 28, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // ========================================== | |
| // DRAW CAPTAIN AMERICA FLYING UPWARD (Bottom Wing of Triangle Formation) | |
| // ========================================== | |
| const capX = mainImX - 220; | |
| const capY = baseLeaderY + 95; | |
| const capW = 165; | |
| const capH = 240; | |
| // Mjolnir Hammer (Right Hand), Shield (Left Hand), Chest & Feet Anchors | |
| const mjolnirX = capX + capW * 0.12; | |
| const mjolnirY = capY + capH * 0.44; | |
| const shieldX = capX + capW * 0.85; | |
| const shieldY = capY + capH * 0.18; | |
| const capChestX = capX + capW * 0.46; | |
| const capChestY = capY + capH * 0.36; | |
| // 1. Draw Crackling Lightning Bolts from Mjolnir Hammer in Cap's Hand | |
| const capBolts = (analyser && dataArray && isRecording) ? 7 : 4; | |
| for (let b = 0; b < capBolts; b++) { | |
| const targetAngle = Math.random() * Math.PI * 2; | |
| const boltLength = 25 + Math.random() * 55; | |
| const endX = mjolnirX + Math.cos(targetAngle) * boltLength; | |
| const endY = mjolnirY + Math.sin(targetAngle) * boltLength; | |
| drawLightningArc(mainCtx, mjolnirX, mjolnirY, endX, endY, 11); | |
| } | |
| // Crackle from Mjolnir across right arm to Chest Star and up to Shield | |
| drawLightningArc(mainCtx, mjolnirX, mjolnirY, capChestX, capChestY, 10); | |
| drawLightningArc(mainCtx, mjolnirX, mjolnirY, shieldX, shieldY, 13); | |
| // 2. Spawn Electric Thunder Spark Particles from Mjolnir Hand | |
| for (let s = 0; s < 4; s++) { | |
| capSparks.push({ | |
| x: mjolnirX + (Math.random() - 0.5) * 24, | |
| y: mjolnirY + (Math.random() - 0.5) * 24, | |
| vx: -(2.0 + Math.random() * 3.8), | |
| vy: (Math.random() - 0.5) * 3.2, | |
| radius: 1.4 + Math.random() * 2.8, | |
| color: Math.random() > 0.4 ? "#00e5ff" : "#ffffff", | |
| life: 1.0, | |
| decay: 0.03 + Math.random() * 0.04 | |
| }); | |
| } | |
| // Render Cap's hand electric sparks | |
| for (let i = capSparks.length - 1; i >= 0; i--) { | |
| const sp = capSparks[i]; | |
| sp.x += sp.vx; | |
| sp.y += sp.vy; | |
| sp.life -= sp.decay; | |
| if (sp.life <= 0) { | |
| capSparks.splice(i, 1); | |
| continue; | |
| } | |
| mainCtx.save(); | |
| mainCtx.globalAlpha = sp.life; | |
| mainCtx.fillStyle = sp.color; | |
| mainCtx.shadowColor = "#00e5ff"; | |
| mainCtx.shadowBlur = 10; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(sp.x, sp.y, sp.radius * sp.life, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| // 3. Draw Luminous Captain America Sprite with Electric Aura & Mjolnir Flare | |
| if (capLoaded) { | |
| mainCtx.save(); | |
| mainCtx.filter = "brightness(1.42) contrast(1.3) saturate(1.35) drop-shadow(0 0 22px rgba(0, 229, 255, 0.95))"; | |
| mainCtx.drawImage(capImg, capX, capY, capW, capH); | |
| mainCtx.restore(); | |
| // Mjolnir Hand Thunder Glow Lens Flare | |
| mainCtx.save(); | |
| const mjolnirGrad = mainCtx.createRadialGradient(mjolnirX, mjolnirY, 0, mjolnirX, mjolnirY, 26); | |
| mjolnirGrad.addColorStop(0, "rgba(255, 255, 255, 1)"); | |
| mjolnirGrad.addColorStop(0.35, "rgba(0, 229, 255, 0.9)"); | |
| mjolnirGrad.addColorStop(1, "rgba(0, 229, 255, 0)"); | |
| mainCtx.fillStyle = mjolnirGrad; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(mjolnirX, mjolnirY, 26, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| // Shield Reflection Electric Sparkle | |
| mainCtx.save(); | |
| const shieldGrad = mainCtx.createRadialGradient(shieldX, shieldY, 0, shieldX, shieldY, 20); | |
| shieldGrad.addColorStop(0, "rgba(255, 255, 255, 0.9)"); | |
| shieldGrad.addColorStop(0.4, "rgba(0, 229, 255, 0.6)"); | |
| shieldGrad.addColorStop(1, "rgba(0, 229, 255, 0)"); | |
| mainCtx.fillStyle = shieldGrad; | |
| mainCtx.beginPath(); | |
| mainCtx.arc(shieldX, shieldY, 20, 0, Math.PI * 2); | |
| mainCtx.fill(); | |
| mainCtx.restore(); | |
| } | |
| requestAnimationFrame(drawMainPageIronMan); | |
| } | |
| // Captain America Sprite Loader | |
| const capImg = new Image(); | |
| capImg.src = "/cap.png"; | |
| let capLoaded = false; | |
| capImg.onload = () => { capLoaded = true; }; | |
| let capX = -650; | |
| const capSparks = []; | |
| // Thor Sprite & Lightning Helper | |
| const thorImg = new Image(); | |
| thorImg.src = "/thor.png"; | |
| let thorLoaded = false; | |
| thorImg.onload = () => { thorLoaded = true; }; | |
| let thorX = -450; | |
| const thorSparks = []; | |
| function drawLightningArc(c, x1, y1, x2, y2, roughness = 14) { | |
| const dx = x2 - x1; | |
| const dy = y2 - y1; | |
| const dist = Math.hypot(dx, dy); | |
| const steps = Math.max(4, Math.floor(dist / 14)); | |
| c.beginPath(); | |
| c.moveTo(x1, y1); | |
| let curX = x1; | |
| let curY = y1; | |
| for (let i = 1; i < steps; i++) { | |
| const t = i / steps; | |
| const targetX = x1 + dx * t; | |
| const targetY = y1 + dy * t; | |
| const offset = (Math.random() - 0.5) * roughness; | |
| const nx = -dy / (dist || 1); | |
| const ny = dx / (dist || 1); | |
| curX = targetX + nx * offset; | |
| curY = targetY + ny * offset; | |
| c.lineTo(curX, curY); | |
| } | |
| c.lineTo(x2, y2); | |
| c.save(); | |
| c.strokeStyle = "rgba(0, 229, 255, 0.95)"; | |
| c.lineWidth = 3.5; | |
| c.shadowColor = "#00e5ff"; | |
| c.shadowBlur = 16; | |
| c.stroke(); | |
| c.strokeStyle = "#ffffff"; | |
| c.lineWidth = 1.6; | |
| c.stroke(); | |
| c.restore(); | |
| } | |
| drawMainPageIronMan(); | |
| </script> | |
| </body> | |
| </html> | |