nishtha711's picture
Upload 5 files
17caec4 verified
Raw
History Blame Contribute Delete
14 kB
<!doctype html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Message Router — an inbox that reasons</title>
<meta name="description" content="An interactive WhatsApp replica showing a real AI notification-routing pipeline: 93.3% accuracy, 32 API calls for 110 messages, $0 cost. Ranked 18/1983 at HackerRank Orchestrate." />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧭</text></svg>" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="blobs" aria-hidden="true">
<div class="blob b1"></div><div class="blob b2"></div><div class="blob b3"></div><div class="blob b4"></div>
</div>
<header class="topbar">
<div class="brand"><span class="mark">🧭</span><span>Message&nbsp;Router</span></div>
<button id="themeToggle" class="theme-toggle" aria-label="Toggle dark mode"></button>
</header>
<section class="hero">
<div class="hero-copy reveal">
<p class="eyebrow">HackerRank Orchestrate · Ranked <span id="statRank">18 / 1,983</span></p>
<h1>Every WhatsApp message,<br>routed like <em>a person</em> would.</h1>
<p class="lede">An AI pipeline that decides what interrupts you now, what waits for later, and what gets muted — reasoning over text, image posters and voice notes, with hard safety rules a language model can't be talked out of. This isn't a mockup: every verdict below is a real, precomputed output of the actual routing run. Tap in.</p>
<div class="stat-row">
<div class="stat-chip" style="--rot:-4deg"><b id="statAccuracy">93.3%</b><span>action accuracy</span></div>
<div class="stat-chip" style="--rot:3deg"><b id="statCalls">32 / 110</b><span>API calls for messages</span></div>
<div class="stat-chip" style="--rot:-2deg"><b id="statCost">$0</b><span>cost, free tier only</span></div>
<div class="stat-chip" style="--rot:5deg"><b id="statCaught">15</b><span>false claims self-caught</span></div>
</div>
</div>
<div class="hero-phone-wrap reveal">
<div class="callout c1">Tap any dashed message → see the full 5-phase trace<span class="arrow">↳ real signals, not vibes</span></div>
<div class="callout c2">Switch inboxes — same sender, different verdict, on purpose<span class="arrow">↳ personalization is the whole point</span></div>
<div class="phone-frame-outer">
<div class="stage-controls">
<label for="userSwitch">Viewing inbox as</label>
<select id="userSwitch" aria-label="Switch device owner"></select>
</div>
<div class="phone" id="phone">
<div class="notch"></div>
<div class="screen">
<div class="statusbar">
<span id="clock">9:41</span>
<span class="icons">📶 🔋</span>
</div>
<div id="viewChatList" class="view chat-list-view">
<div class="wa-header list-header">
<span class="wa-title">Chats</span>
<div class="header-icons">
<button id="searchIconBtn" aria-label="Search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
</button>
<button id="menuBtn" aria-label="Menu">
<svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>
</button>
<div class="dropdown-menu" id="menuDropdown">
<button data-action="theme">🌓 <span>Toggle theme</span></button>
<button data-action="shuffle">🔀 <span>Shuffle inbox</span></button>
<button data-action="router">🧭 <span>Open Router HQ</span></button>
<div class="dm-divider"></div>
<button data-action="diagram">📐 <span>View architecture</span></button>
</div>
</div>
</div>
<div class="filter-row" id="filterRow">
<button class="filter-pill active" data-filter="all">All</button><button class="filter-pill" data-filter="unread">Unread</button><button class="filter-pill" data-filter="group">Groups</button>
</div>
<div class="search-bar" id="searchBar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input id="searchInput" type="text" placeholder="Search chats" autocomplete="off" />
<button class="search-clear" id="searchClear" aria-label="Clear search"></button>
</div>
<div class="chat-list" id="chatList"></div>
<div class="nav-stub hidden" id="navStub"></div>
<div class="bottom-nav">
<button class="nav-item active" data-tab="chats"><span class="nav-ico">💬</span>Chats</button>
<button class="nav-item" data-tab="updates"><span class="nav-ico">🔄</span>Updates</button>
<button class="nav-item" data-tab="communities"><span class="nav-ico">👥</span>Communities</button>
<button class="nav-item" data-tab="calls"><span class="nav-ico">📞</span>Calls</button>
</div>
</div>
<div id="viewThread" class="view thread-view hidden">
<div class="wa-header thread-header">
<button class="back-btn" id="backBtn" aria-label="Back"></button>
<button class="avatar" id="threadAvatar" aria-label="Contact info" style="border:none;cursor:pointer;padding:0"></button>
<button class="thread-title" id="threadTitleBtn" aria-label="Contact info" style="background:none;border:none;text-align:left;cursor:pointer;padding:0;color:inherit">
<span class="t-name" id="threadName"></span>
<span class="t-sub" id="threadSubtitle"></span>
</button>
<div class="header-icons">
<button id="videoBtn" aria-label="Video call"><svg viewBox="0 0 24 24"><path d="M17 10.5V7a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h12a1 1 0 001-1v-3.5l4 4v-11l-4 4z"/></svg></button>
<button id="callBtn" aria-label="Voice call"><svg viewBox="0 0 24 24"><path d="M6.6 10.8c1.4 2.7 3.6 4.9 6.3 6.3l2.1-2.1c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.5.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.4 21 3 13.6 3 4.5c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.2.2 2.4.6 3.5.1.4 0 .8-.2 1L6.6 10.8z"/></svg></button>
</div>
<div class="info-pop" id="infoPop"></div>
</div>
<div class="thread-messages" id="threadMessages"></div>
<div class="composer" id="composer"></div>
</div>
<div class="toast-stack" id="toastStack"></div>
</div>
</div>
</div>
</div>
</section>
<section class="pipeline-overview">
<h2>How a message actually gets routed</h2>
<p class="sub">Five phases, most of them deterministic. The model only speaks once it's the last honest option.</p>
<figure class="diagram-card" id="archCard">
<svg viewBox="0 0 1100 170" role="img" aria-label="Pipeline flow: dataset feeds a per-user bundle, then media OCR/transcription, then rules. Rules run deterministically; an override branches above the rules-to-router step and can force the verdict, while a separate short path shows the router still receives the override's evidence. The router calls the LLM once per user, then normalize validates and semantically checks the output before it becomes a CSV row." font-family="ui-monospace, Consolas, monospace" font-size="11.5">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0L10 5L0 10z" fill="currentColor" />
</marker>
</defs>
<g class="arch-node" id="an-source" fill="currentColor">
<text x="55" y="108" text-anchor="middle" font-size="18">📄</text>
<text x="55" y="130" text-anchor="middle" font-size="9.5" opacity="0.7">dataset/*.csv</text>
</g>
<line class="arch-edge" id="ae-source-bundle" x1="95" y1="109" x2="130" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-bundle" fill="currentColor">
<rect x="130" y="88" width="150" height="42" rx="10" fill="none" stroke="currentColor" opacity="0.8"/>
<text x="205" y="106" text-anchor="middle" font-weight="700">1 · bundle.py</text>
<text x="205" y="120" text-anchor="middle" opacity="0.6">one block per user</text>
</g>
<line class="arch-edge" id="ae-bundle-media" x1="280" y1="109" x2="314" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-media" fill="currentColor">
<rect x="314" y="88" width="150" height="42" rx="10" fill="none" stroke="currentColor" opacity="0.8"/>
<text x="389" y="106" text-anchor="middle" font-weight="700">2 · media.py</text>
<text x="389" y="120" text-anchor="middle" opacity="0.6">OCR + transcript, cached</text>
</g>
<line class="arch-edge" id="ae-media-rules" x1="464" y1="109" x2="498" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-rules" fill="currentColor">
<rect x="498" y="88" width="150" height="42" rx="10" fill="none" stroke="currentColor" opacity="0.8"/>
<text x="573" y="106" text-anchor="middle" font-weight="700">3 · rules.py</text>
<text x="573" y="120" text-anchor="middle" opacity="0.6">4 hard overrides</text>
</g>
<!-- override branches above the rules→router step; two short paths fan out from it -->
<line class="arch-edge" id="ae-override-stub" x1="665" y1="50" x2="665" y2="85" stroke="currentColor" stroke-width="1.6" stroke-dasharray="3 3" marker-end="url(#arrow)" fill="none" />
<g class="arch-node hot" id="an-override" fill="currentColor">
<rect x="580" y="8" width="170" height="42" rx="10" fill="none" stroke="var(--danger, currentColor)" stroke-width="2"/>
<text x="665" y="26" text-anchor="middle" font-weight="700">override fires?</text>
<text x="665" y="40" text-anchor="middle" opacity="0.6">verdict skips the LLM</text>
</g>
<line class="arch-edge" id="ae-rules-router" x1="648" y1="109" x2="682" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-router" fill="currentColor">
<rect x="682" y="88" width="150" height="42" rx="10" fill="none" stroke="currentColor" opacity="0.8"/>
<text x="757" y="106" text-anchor="middle" font-weight="700">4 · router.py</text>
<text x="757" y="120" text-anchor="middle" opacity="0.6">1 call / user, T=0</text>
</g>
<line class="arch-edge" id="ae-router-normalize" x1="832" y1="109" x2="866" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-normalize" fill="currentColor">
<rect x="866" y="88" width="150" height="42" rx="10" fill="none" stroke="currentColor" opacity="0.8"/>
<text x="941" y="106" text-anchor="middle" font-weight="700">5 · normalize.py</text>
<text x="941" y="120" text-anchor="middle" opacity="0.6">semantic check</text>
</g>
<line class="arch-edge" id="ae-normalize-output" x1="1016" y1="109" x2="1050" y2="109" stroke="currentColor" stroke-width="1.6" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-output" fill="currentColor">
<text x="1063" y="108" text-anchor="middle" font-size="18">📤</text>
<text x="1063" y="130" text-anchor="middle" font-size="9.5" opacity="0.7">output.csv</text>
</g>
<path class="arch-edge" id="ae-evidence" d="M720 50 Q745 68 757 87" stroke="currentColor" stroke-width="1.6" stroke-dasharray="4 4" marker-end="url(#arrow)" fill="none" />
<g class="arch-node" id="an-evidence-note" fill="currentColor">
<text x="855" y="20" text-anchor="middle" font-size="10" opacity="0.75">override's own evidence</text>
<text x="855" y="33" text-anchor="middle" font-size="10" opacity="0.75">still reaches router.py</text>
</g>
</svg>
<figcaption>Rules can force the verdict on 4 hard-override conditions (OTP/credential asks, lookalike-domain payments, router-injection attempts, chain forwards) — but the router's evidence selection survives the override, since evidence is scored output, not a safety decision.</figcaption>
<button class="diagram-replay" id="diagramReplay">🔁 Replay</button>
</figure>
</section>
<footer class="site-footer">
<p>Built for HackerRank Orchestrate, August 2026 · ranked 18 / 1,983 · every number on this page comes from the committed run, nothing is fabricated for the demo.</p>
</footer>
<div id="sheetOverlay" class="sheet-overlay hidden"></div>
<div id="pipelineSheet" class="pipeline-sheet hidden" role="dialog" aria-modal="true" aria-label="Routing trace">
<div class="sheet-grab"></div>
<div class="sheet-head">
<div class="sh-top">
<div>
<h3 id="sheetTitle">How this got routed</h3>
<div class="sh-msg" id="sheetMsgPreview"></div>
</div>
<button class="sheet-close" id="sheetClose" aria-label="Close"></button>
</div>
<div class="action-badge" id="actionBadge"></div>
<div class="conf-gauge" id="confGauge"></div>
</div>
<div class="sheet-body">
<div class="flow">
<div class="flow-rail" id="flowRail"></div>
<div class="flow-steps" id="flowSteps"></div>
</div>
</div>
</div>
<script src="app.js"></script>
<script>
var _clk = document.getElementById("clock");
function _tick(){ if(_clk){ _clk.textContent = new Date().toLocaleTimeString([], {hour:"2-digit", minute:"2-digit", hour12:false}); } }
_tick(); setInterval(_tick, 30000);
</script>
</body>
</html>