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&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>