File size: 13,985 Bytes
17caec4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | <!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 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>
|