vector-backend / web_ui /index.html
rishik1111's picture
Configure Option 3 decoupled Vercel deployment with API rewrites and dynamic URL resolution
fb96eeb
Raw
History Blame Contribute Delete
87.4 kB
<!DOCTYPE html>
<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 !important;
}
html, body {
color-scheme: light !important;
background-color: #0A5C36 !important;
color: #111827 !important;
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 !important;
}
.text-black {
color: #000000 !important;
}
.text-slate-800, .text-slate-700, .text-slate-600 {
color: #1F2937 !important;
}
/* Dark containers in Knowledge Sea */
.sea-grid, .bg-jungle-950, .bg-jungle-900, .bg-jungle-800 {
color: #FEF8EA !important;
}
.sea-grid .bg-sand-100 {
background-color: #FEF8EA !important;
color: #111827 !important;
}
.sea-grid .bg-sand-100 p, .sea-grid .bg-sand-100 span, .sea-grid .bg-sand-100 div {
color: #111827 !important;
}
/* 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 !important;
box-shadow: 6px 6px 0px #000 !important;
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 !important;
}
/* 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 !important;
}
/* 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 !important;
opacity: 1 !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
.ocean-dark-bg {
background-color: #053B22 !important;
color: #FFFFFF !important;
}
.beach-sand-bg {
background-color: #FFFBEA !important;
color: #111827 !important;
opacity: 1 !important;
}
/* 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 !important;
}
/* 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 !important;
}
.ocean-deck-bg {
background-color: #053B22 !important;
color: #FFFFFF !important;
}
.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 &lt;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>