Keyonll/gemma-4-webgpu-kernels
0
1<!doctype html>2<html lang="en">3<head>4<meta charset="utf-8" />5<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />6<meta name="color-scheme" content="dark" />7<title>Gemma 4 E2B · WebGPU</title>8<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>">9<meta name="description" content="Gemma 4 E2B (QAT Mobile) running fully in your browser on WebGPU. Every kernel written and optimized by Fable 5." />10<link rel="preconnect" href="https://fonts.googleapis.com">11<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>12<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">13<script type="importmap">14{15 "imports": {16 "three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",17 "three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/"18 }19}20</script>21<style>22 :root {23 --bg: #020203;24 --t1: rgba(255, 255, 255, 0.92);25 --t2: rgba(255, 255, 255, 0.55);26 --t3: rgba(255, 255, 255, 0.32);27 --t4: rgba(255, 255, 255, 0.20);28 --line: rgba(255, 255, 255, 0.08);29 --line-soft: rgba(255, 255, 255, 0.06);30 --panel: rgba(255, 255, 255, 0.03);31 --panel-hover: rgba(255, 255, 255, 0.06);32 --ok: #64ffa0;33 --warn: #ffcd6b;34 --danger: #ff7a6b;35 --user-bubble: rgba(255, 255, 255, 0.065);36 37 --display: "Instrument Serif", Georgia, "Times New Roman", serif;38 --body: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;39 --mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, monospace;40 --maxw: 820px;41 }42 43 * { box-sizing: border-box; margin: 0; padding: 0; }44 45 html { height: 100%; }46 47 body {48 background: var(--bg);49 color: var(--t1);50 font-family: var(--body);51 font-size: 16px;52 font-weight: 400;53 line-height: 1.6;54 -webkit-font-smoothing: antialiased;55 height: 100dvh;56 overflow-x: hidden;57 overflow-y: scroll;58 scroll-behavior: smooth;59 }60 61 button, textarea, input { font: inherit; color: inherit; }62 button { appearance: none; background: none; border: 0; cursor: pointer; }63 a { color: inherit; text-decoration: none; }64 65 ::selection { background: rgba(255, 255, 255, 0.18); }66 67 body::-webkit-scrollbar { width: 0; }68 69 .screen { min-height: 100dvh; position: relative; width: 100%; }70 71 /* ====================================================================== */72 /* LANDING */73 /* ====================================================================== */74 75 #landing {76 display: flex;77 flex-direction: column;78 overflow: hidden;79 }80 81 #crt-frame {82 position: absolute;83 inset: 0;84 z-index: 0;85 overflow: hidden;86 pointer-events: none;87 opacity: 0;88 transition: opacity 2.4s cubic-bezier(0.25, 0.1, 0.25, 1) 0.6s;89 }90 #crt-frame.visible { opacity: 1; }91 #crt-frame canvas { display: block; width: 100% !important; height: 100% !important; pointer-events: none; }92 93 .hero-fade {94 position: absolute;95 bottom: 0; left: 0;96 width: 100%; height: 68%;97 background: linear-gradient(to top, var(--bg) 0%, var(--bg) 14%, rgba(2, 2, 3, 0.55) 52%, transparent 100%);98 z-index: 1;99 pointer-events: none;100 opacity: 0;101 transition: opacity 2s ease 1s;102 }103 .hero-fade.in { opacity: 1; }104 105 .hero-overlay {106 position: relative;107 z-index: 2;108 flex: 1;109 display: flex;110 flex-direction: column;111 pointer-events: none;112 min-height: 100dvh;113 }114 115 /* used by the chat header's back-to-top link */116 .brand-name {117 font-family: var(--mono);118 font-size: 12px;119 font-weight: 500;120 letter-spacing: 0.22em;121 text-transform: uppercase;122 color: var(--t1);123 }124 125 /* hero body — anchored to the bottom, over the darkened fade for legibility */126 .hero-content {127 flex: 1;128 display: flex;129 flex-direction: column;130 justify-content: flex-end;131 padding: 0 48px 18px;132 pointer-events: none;133 }134 .eyebrow {135 display: inline-flex;136 align-items: center;137 gap: 10px;138 width: fit-content;139 font-family: var(--mono);140 font-size: 11px;141 letter-spacing: 0.2em;142 text-transform: uppercase;143 color: var(--t3);144 margin-bottom: 22px;145 text-shadow: 0 2px 16px var(--bg);146 }147 .eyebrow::before {148 content: "";149 flex-shrink: 0;150 width: 7px; height: 7px; border-radius: 50%;151 background: var(--ok);152 box-shadow: 0 0 12px rgba(100, 255, 160, 0.5);153 animation: statusPulse 2.4s ease-in-out infinite;154 }155 .hero-h1 {156 font-family: var(--display);157 font-size: clamp(44px, 7.6vw, 100px);158 font-weight: 400;159 line-height: 0.98;160 letter-spacing: -0.015em;161 color: #fff;162 margin-bottom: 30px;163 max-width: 26ch;164 text-shadow: 0 3px 40px var(--bg), 0 1px 14px rgba(2, 2, 3, 0.85);165 }166 .hero-h1 .thin { color: var(--t3); }167 168 /* bottom row: description + stats (left), actions + credit (right) */169 .hero-row {170 display: flex;171 align-items: flex-end;172 justify-content: space-between;173 gap: 56px;174 }175 .hero-main { display: flex; flex-direction: column; min-width: 0; }176 .hero-sub {177 font-size: 15px;178 font-weight: 300;179 line-height: 1.7;180 color: var(--t2);181 max-width: 52ch;182 text-shadow: 0 2px 16px var(--bg), 0 1px 30px var(--bg);183 margin-bottom: 26px;184 }185 .hero-sub b { color: var(--t1); font-weight: 500; }186 .hero-stats { display: flex; flex-wrap: wrap; gap: 36px; }187 .stat { display: flex; flex-direction: column; gap: 5px; }188 .stat-val {189 font-family: var(--display);190 font-size: 30px;191 font-weight: 400;192 letter-spacing: -0.01em;193 color: var(--t1);194 line-height: 1;195 text-shadow: 0 2px 16px var(--bg);196 }197 .stat-label {198 font-family: var(--mono);199 font-size: 10.5px;200 letter-spacing: 0.12em;201 text-transform: uppercase;202 color: var(--t3);203 }204 205 /* right column */206 .hero-side {207 display: flex;208 flex-direction: column;209 align-items: flex-end;210 gap: 18px;211 flex-shrink: 0;212 text-align: right;213 }214 .hero-actions {215 display: flex;216 align-items: center;217 gap: 12px;218 pointer-events: auto;219 }220 221 .btn-primary {222 position: relative;223 display: inline-flex;224 align-items: center;225 gap: 10px;226 padding: 14px 28px;227 color: #000;228 font-size: 13.5px;229 font-weight: 500;230 letter-spacing: 0.01em;231 border-radius: 100px;232 border: 1px solid #fff;233 white-space: nowrap;234 overflow: hidden;235 isolation: isolate;236 transition: opacity 0.2s ease;237 }238 .btn-primary::before {239 content: "";240 position: absolute;241 inset: 0;242 background: #fff;243 border-radius: inherit;244 transform: scaleX(1);245 transform-origin: left center;246 transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);247 z-index: -1;248 }249 .btn-primary:hover:not(:disabled)::before { transform: scaleX(0); transform-origin: right center; }250 .btn-primary:hover:not(:disabled) { color: #fff; }251 .btn-primary svg { width: 14px; height: 14px; }252 .btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }253 254 .btn-secondary {255 display: inline-flex;256 align-items: center;257 gap: 9px;258 padding: 14px 26px;259 color: var(--t2);260 font-size: 13.5px;261 font-weight: 400;262 border-radius: 100px;263 border: 1px solid var(--line);264 white-space: nowrap;265 transition: border-color 0.25s ease, color 0.25s ease;266 }267 .btn-secondary:hover { border-color: rgba(255, 255, 255, 0.4); color: var(--t1); }268 .btn-secondary svg { width: 12px; height: 12px; }269 270 /* kernel credit + experimental note, in the right column */271 .hero-foot {272 display: flex;273 flex-direction: column;274 align-items: flex-end;275 gap: 8px;276 pointer-events: auto;277 }278 .kernel-note-cta {279 width: fit-content;280 font-size: 13px;281 font-weight: 400;282 color: var(--t2);283 border-bottom: 1px solid var(--line);284 padding-bottom: 2px;285 transition: color 0.2s ease, border-color 0.2s ease;286 }287 .kernel-note-cta b { color: var(--t1); font-weight: 600; }288 .kernel-note-cta:hover { color: var(--t1); border-color: rgba(255, 255, 255, 0.4); }289 .hero-foot-note {290 font-family: var(--mono);291 font-size: 10px;292 letter-spacing: 0.06em;293 color: var(--t4);294 }295 296 /* scroll cue — sits at the very bottom of the hero, below the content */297 .scroll-cue {298 flex: 0 0 auto;299 align-self: center;300 display: flex;301 flex-direction: column;302 align-items: center;303 gap: 9px;304 padding-bottom: 18px;305 pointer-events: none;306 opacity: 0;307 transition: opacity 0.8s ease 1.8s;308 }309 .scroll-cue.in { opacity: 1; }310 .scroll-cue span {311 font-family: var(--mono);312 font-size: 9.5px;313 letter-spacing: 0.2em;314 text-transform: uppercase;315 color: var(--t4);316 }317 .scroll-cue-line { width: 1px; height: 30px; background: var(--line); position: relative; overflow: hidden; }318 .scroll-cue-line::after {319 content: "";320 position: absolute;321 top: -100%; left: 0;322 width: 100%; height: 100%;323 background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.4), transparent);324 animation: scrollDrop 2s ease-in-out infinite;325 }326 327 /* ====================================================================== */328 /* CHAT */329 /* ====================================================================== */330 331 #chat {332 display: flex;333 flex-direction: column;334 height: 100dvh;335 background:336 radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.035), transparent 55%),337 var(--bg);338 }339 340 .chat-head {341 position: relative;342 flex: 0 0 auto;343 border-bottom: 1px solid var(--line-soft);344 background: rgba(2, 2, 3, 0.72);345 backdrop-filter: blur(12px);346 -webkit-backdrop-filter: blur(12px);347 }348 .chat-head-inner {349 margin: 0 auto;350 max-width: var(--maxw);351 padding: 16px 28px;352 display: flex;353 align-items: center;354 gap: 16px;355 }356 .to-top {357 display: inline-flex;358 align-items: center;359 gap: 9px;360 flex-shrink: 0;361 transition: opacity 0.2s ease;362 }363 .to-top:hover { opacity: 0.7; }364 .to-top svg { width: 13px; height: 13px; color: var(--t3); }365 .to-top .brand-name { font-size: 11px; }366 367 .status {368 flex: 1;369 min-width: 0;370 display: flex;371 align-items: center;372 gap: 9px;373 color: var(--t2);374 font-size: 13px;375 }376 .status-dot {377 width: 7px; height: 7px; border-radius: 50%;378 background: var(--t4);379 flex-shrink: 0;380 transition: background 0.3s ease, box-shadow 0.3s ease;381 }382 .status.loading .status-dot { background: var(--warn); box-shadow: 0 0 10px rgba(255, 205, 107, 0.5); }383 .status.ready .status-dot { background: var(--ok); box-shadow: 0 0 10px rgba(100, 255, 160, 0.5); }384 .status.busy .status-dot { background: var(--ok); box-shadow: 0 0 10px rgba(100, 255, 160, 0.5); animation: statusPulse 1.1s ease-in-out infinite; }385 .status.error .status-dot { background: var(--danger); box-shadow: 0 0 10px rgba(255, 122, 107, 0.5); }386 .status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-variant-numeric: tabular-nums; }387 .status-text strong { color: var(--t1); font-weight: 500; }388 389 .chat-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }390 .head-btn {391 display: inline-flex;392 align-items: center;393 gap: 7px;394 padding: 8px 15px;395 font-family: var(--mono);396 font-size: 11px;397 letter-spacing: 0.08em;398 text-transform: uppercase;399 color: var(--t2);400 border: 1px solid var(--line);401 border-radius: 100px;402 transition: border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;403 }404 .head-btn:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.35); color: var(--t1); }405 .head-btn:disabled { opacity: 0.35; cursor: not-allowed; }406 .head-btn[hidden] { display: none; }407 .head-btn.solid { background: #fff; color: #000; border-color: #fff; }408 .head-btn.solid:hover:not(:disabled) { opacity: 0.85; color: #000; }409 410 /* Absolutely positioned over the header's bottom edge so it can never affect layout —411 no element shifts when loading starts or ends; the fill just fades out on completion. */412 .bar {413 position: absolute;414 left: 0; right: 0; bottom: 0;415 height: 2px;416 overflow: hidden;417 pointer-events: none;418 }419 .bar > div {420 height: 100%;421 width: 0%;422 background: linear-gradient(90deg, var(--t2), #fff);423 transition: opacity 0.4s ease; /* width is animated per-frame in JS (see stepProgressBar) */424 }425 .bar.done > div { opacity: 0; }426 427 .thread-scroll {428 flex: 1 1 auto;429 min-height: 0;430 overflow-y: auto;431 scroll-behavior: smooth;432 }433 .thread {434 margin: 0 auto;435 max-width: var(--maxw);436 min-height: 100%;437 padding: 40px 28px 24px;438 display: flex;439 flex-direction: column;440 gap: 32px;441 }442 443 .welcome {444 margin: auto 0;445 text-align: center;446 padding: 4vh 0;447 animation: rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;448 }449 .welcome h2 {450 font-family: var(--display);451 font-size: clamp(30px, 6vw, 46px);452 font-weight: 400;453 line-height: 1.05;454 color: #fff;455 margin-bottom: 14px;456 }457 .welcome h2 .thin { color: var(--t3); }458 .welcome p { color: var(--t2); max-width: 46ch; margin: 0 auto 28px; font-weight: 300; }459 .seeds { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }460 .seed {461 padding: 9px 16px;462 font-size: 13.5px;463 color: var(--t2);464 background: var(--panel);465 border: 1px solid var(--line);466 border-radius: 100px;467 transition: border-color 0.2s ease, color 0.2s ease, background 0.2s ease, transform 0.15s ease, opacity 0.2s ease;468 }469 .seed:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.3); color: var(--t1); background: var(--panel-hover); transform: translateY(-1px); }470 .seed:disabled { opacity: 0.4; cursor: not-allowed; }471 472 .msg { display: flex; flex-direction: column; animation: rise 0.4s cubic-bezier(0.2, 0.7, 0.2, 1) both; }473 .role {474 display: flex;475 align-items: center;476 gap: 8px;477 font-family: var(--mono);478 font-size: 10.5px;479 letter-spacing: 0.16em;480 text-transform: uppercase;481 margin-bottom: 10px;482 }483 .role::before { content: ""; width: 5px; height: 5px; border-radius: 50%; }484 .msg.user { align-items: flex-end; }485 .msg.user .role { color: var(--t3); }486 .msg.user .role::before { background: var(--t3); }487 .msg.assistant .role { color: var(--ok); }488 .msg.assistant .role::before { background: var(--ok); }489 490 .bubble { overflow-wrap: anywhere; }491 .bubble.user {492 background: var(--user-bubble);493 border: 1px solid var(--line);494 border-radius: 16px 16px 4px 16px;495 padding: 12px 17px;496 line-height: 1.55;497 max-width: min(82%, 600px);498 white-space: pre-wrap;499 color: var(--t1);500 }501 .bubble.assistant { font-size: 16px; line-height: 1.72; color: var(--t1); max-width: 100%; }502 .bubble.assistant > :first-child { margin-top: 0; }503 .bubble.assistant > :last-child { margin-bottom: 0; }504 .bubble.assistant p { margin: 0 0 0.9em; }505 .bubble.assistant strong { color: #fff; font-weight: 600; }506 .bubble.assistant em { font-style: italic; }507 .bubble.assistant a { color: #8ab4ff; text-decoration: underline; text-underline-offset: 2px; }508 .bubble.assistant a:hover { color: #aecbff; }509 .bubble.assistant h1, .bubble.assistant h2, .bubble.assistant h3,510 .bubble.assistant h4, .bubble.assistant h5, .bubble.assistant h6 {511 color: #fff; font-weight: 600; line-height: 1.25; margin: 1.3em 0 0.6em;512 }513 .bubble.assistant h1 { font-size: 1.5em; }514 .bubble.assistant h2 { font-size: 1.3em; }515 .bubble.assistant h3 { font-size: 1.13em; }516 .bubble.assistant h4, .bubble.assistant h5, .bubble.assistant h6 { font-size: 1em; }517 .bubble.assistant ul, .bubble.assistant ol { margin: 0 0 0.9em; padding-left: 1.5em; }518 .bubble.assistant li { margin: 0.25em 0; }519 .bubble.assistant li::marker { color: var(--t3); }520 .bubble.assistant blockquote {521 margin: 0 0 0.9em; padding: 2px 0 2px 16px;522 border-left: 2px solid var(--line); color: var(--t2);523 }524 .bubble.assistant hr { border: 0; border-top: 1px solid var(--line); margin: 1.3em 0; }525 .bubble.assistant code {526 font-family: var(--mono);527 font-size: 0.85em;528 background: var(--panel);529 border: 1px solid var(--line);530 border-radius: 5px;531 padding: 1px 5px;532 }533 .bubble.assistant pre {534 margin: 0 0 0.9em;535 padding: 14px 16px;536 background: #0a0a0c;537 border: 1px solid var(--line);538 border-radius: 12px;539 overflow-x: auto;540 }541 .bubble.assistant pre code { background: none; border: 0; padding: 0; font-size: 0.82em; line-height: 1.6; }542 .bubble.assistant table { border-collapse: collapse; margin: 0 0 0.9em; font-size: 0.92em; display: block; overflow-x: auto; }543 .bubble.assistant th, .bubble.assistant td { border: 1px solid var(--line); padding: 6px 11px; text-align: left; }544 .bubble.assistant th { background: var(--panel); color: var(--t1); font-weight: 600; }545 .bubble.assistant pre::-webkit-scrollbar, .bubble.assistant table::-webkit-scrollbar { height: 8px; }546 .bubble.assistant pre::-webkit-scrollbar-thumb, .bubble.assistant table::-webkit-scrollbar-thumb { background: var(--line); border-radius: 8px; }547 548 .meta {549 font-family: var(--mono);550 font-size: 10.5px;551 letter-spacing: 0.04em;552 color: var(--t3);553 margin-top: 12px;554 }555 556 .thinking { display: inline-flex; gap: 5px; padding: 4px 0; }557 .thinking span { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); opacity: 0.5; animation: bob 1.3s ease-in-out infinite; }558 .thinking span:nth-child(2) { animation-delay: 0.18s; }559 .thinking span:nth-child(3) { animation-delay: 0.36s; }560 561 .caret { display: inline-block; width: 2px; height: 1.05em; margin-left: 2px; vertical-align: -0.16em; background: var(--ok); animation: blink 1s steps(2) infinite; }562 563 .composer-wrap {564 flex: 0 0 auto;565 background: linear-gradient(rgba(2, 2, 3, 0), var(--bg) 30%);566 padding-top: 8px;567 }568 .composer { margin: 0 auto; max-width: var(--maxw); padding: 0 28px 22px; }569 .field {570 display: grid;571 grid-template-columns: 1fr 44px;572 align-items: flex-end;573 gap: 8px;574 background: var(--panel);575 border: 1px solid var(--line);576 border-radius: 18px;577 padding: 8px 8px 8px 18px;578 transition: border-color 0.2s ease, background 0.2s ease;579 }580 .field:focus-within { border-color: rgba(255, 255, 255, 0.28); background: rgba(255, 255, 255, 0.05); }581 textarea {582 background: transparent;583 border: 0;584 outline: none;585 resize: none;586 width: 100%;587 min-height: 42px;588 max-height: 180px;589 padding: 8px 0;590 color: var(--t1);591 }592 textarea::placeholder { color: var(--t3); }593 594 .icon-button {595 display: grid;596 place-items: center;597 width: 42px;598 height: 42px;599 border-radius: 13px;600 transition: background 0.2s ease, opacity 0.2s ease, transform 0.1s ease;601 }602 .icon-button svg { width: 19px; height: 19px; }603 .icon-button:active:not(:disabled) { transform: scale(0.94); }604 .icon-button:disabled { opacity: 0.3; cursor: not-allowed; }605 .send-button { background: #fff; color: #000; }606 .send-button:hover:not(:disabled) { opacity: 0.86; }607 .stop-button { background: rgba(255, 122, 107, 0.14); color: var(--danger); border: 1px solid rgba(255, 122, 107, 0.3); display: none; }608 .stop-button:hover:not(:disabled) { background: rgba(255, 122, 107, 0.22); }609 610 .composer-meta {611 display: flex;612 align-items: center;613 justify-content: center;614 gap: 12px;615 margin: 10px 4px 0;616 min-height: 22px;617 font-family: var(--mono);618 font-size: 10.5px;619 letter-spacing: 0.04em;620 text-transform: uppercase;621 color: var(--t3);622 }623 624 .thread-scroll::-webkit-scrollbar { width: 10px; }625 .thread-scroll::-webkit-scrollbar-thumb { background: var(--line); border: 3px solid var(--bg); border-radius: 10px; }626 627 :focus-visible { outline: 1px solid rgba(255, 255, 255, 0.45); outline-offset: 3px; }628 /* The composer already shows focus via .field:focus-within — suppress the textarea's own outline. */629 textarea:focus, textarea:focus-visible { outline: none; }630 631 /* ====================================================================== */632 /* kernels overlay */633 /* ====================================================================== */634 635 .kx {636 position: fixed;637 inset: 0;638 z-index: 50;639 display: flex;640 align-items: center;641 justify-content: center;642 padding: 28px;643 }644 .kx[hidden] { display: none; }645 .kx-backdrop {646 position: absolute;647 inset: 0;648 background: rgba(0, 0, 0, 0.62);649 backdrop-filter: blur(6px);650 -webkit-backdrop-filter: blur(6px);651 animation: kxFade 0.25s ease;652 }653 .kx-panel {654 position: relative;655 display: flex;656 flex-direction: column;657 width: min(1080px, 100%);658 height: min(86vh, 920px);659 background: #0a0a0c;660 border: 1px solid var(--line);661 border-radius: 18px;662 overflow: hidden;663 box-shadow: 0 40px 120px -30px rgba(0, 0, 0, 0.9);664 animation: kxRise 0.3s cubic-bezier(0.2, 0.7, 0.2, 1);665 }666 .kx-head {667 display: flex;668 align-items: flex-start;669 justify-content: space-between;670 gap: 16px;671 padding: 20px 22px;672 border-bottom: 1px solid var(--line-soft);673 }674 .kx-title h3 { font-family: var(--display); font-weight: 400; font-size: 26px; color: #fff; line-height: 1; }675 .kx-sub { display: block; margin-top: 7px; font-size: 12.5px; color: var(--t3); }676 .kx-close {677 display: grid;678 place-items: center;679 width: 34px; height: 34px;680 flex-shrink: 0;681 border-radius: 9px;682 color: var(--t2);683 border: 1px solid var(--line);684 transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;685 }686 .kx-close:hover { color: var(--t1); border-color: rgba(255, 255, 255, 0.35); background: var(--panel); }687 .kx-close svg { width: 15px; height: 15px; }688 .kx-body { flex: 1; display: grid; grid-template-columns: 236px 1fr; min-height: 0; }689 .kx-side {690 position: relative;691 min-width: 0;692 min-height: 0;693 border-right: 1px solid var(--line-soft);694 }695 .kx-list {696 height: 100%;697 overflow-y: auto;698 padding: 10px;699 display: flex;700 flex-direction: column;701 gap: 2px;702 }703 /* fade hinting more kernels are scrollable below; fades out at the end of the list */704 .kx-side::after {705 content: "";706 position: absolute;707 left: 0; right: 1px; bottom: 0;708 height: 54px;709 background: linear-gradient(transparent, #0a0a0c 88%);710 pointer-events: none;711 opacity: 1;712 transition: opacity 0.25s ease;713 }714 .kx-side.at-end::after { opacity: 0; }715 .kx-item {716 flex-shrink: 0;717 text-align: left;718 padding: 9px 12px;719 border-radius: 8px;720 font-family: var(--mono);721 font-size: 12px;722 line-height: 1.5;723 color: var(--t2);724 white-space: nowrap;725 overflow: hidden;726 text-overflow: ellipsis;727 transition: background 0.15s ease, color 0.15s ease;728 }729 .kx-item:hover { background: var(--panel); color: var(--t1); }730 .kx-item.active { background: var(--panel-hover); color: #fff; }731 .kx-view { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }732 .kx-view-head {733 display: flex;734 align-items: center;735 justify-content: space-between;736 gap: 12px;737 padding: 12px 18px;738 border-bottom: 1px solid var(--line-soft);739 }740 .kx-name { font-family: var(--mono); font-size: 13px; color: var(--t1); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }741 .kx-view-actions { display: flex; align-items: center; gap: 14px; flex-shrink: 0; }742 .kx-lines { font-family: var(--mono); font-size: 11px; color: var(--t4); }743 .kx-copy {744 font-family: var(--mono);745 font-size: 11px;746 text-transform: uppercase;747 letter-spacing: 0.08em;748 color: var(--t2);749 border: 1px solid var(--line);750 border-radius: 100px;751 padding: 5px 13px;752 transition: color 0.2s ease, border-color 0.2s ease;753 }754 .kx-copy:hover { color: var(--t1); border-color: rgba(255, 255, 255, 0.35); }755 .kx-code {756 flex: 1;757 min-height: 0;758 min-width: 0;759 overflow: auto;760 margin: 0;761 padding: 18px 20px;762 font-family: var(--mono);763 font-size: 12.5px;764 line-height: 1.65;765 color: var(--t2);766 tab-size: 2;767 }768 .kx-code code { white-space: pre; }769 /* WGSL syntax highlight */770 .k-cm { color: #5d6b6f; font-style: italic; }771 .k-kw { color: #c792ea; }772 .k-ty { color: #6fb3ff; }773 .k-at { color: #ffb074; }774 .k-nu { color: #7ee787; }775 776 /* explanation shown until a kernel is picked (swaps in/out with the source view) */777 .kx-source { flex: 1; min-height: 0; display: flex; flex-direction: column; }778 .kx-source[hidden] { display: none; }779 .kx-intro {780 flex: 1;781 min-height: 0;782 overflow-y: auto;783 display: flex;784 align-items: center;785 justify-content: center;786 padding: 34px 30px;787 }788 .kx-intro[hidden] { display: none; }789 .kx-intro-inner {790 max-width: 540px;791 text-align: center;792 animation: kxIntroRise 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) both;793 }794 795 /* rainbow-shimmer icon — a slowly rotating conic ring + halo in the app's accent palette */796 .kx-spark { position: relative; width: 60px; height: 60px; margin: 0 auto 24px; }797 .kx-spark-ring {798 position: absolute; inset: 0; border-radius: 50%;799 background: conic-gradient(from 0deg, #ff7a6b, #ffcd6b, #64ffa0, #6fb3ff, #c792ea, #ff7a6b);800 animation: kxSpin 6s linear infinite;801 }802 .kx-spark-ring::after { /* blurred halo that rotates with the ring → soft shimmer */803 content: ""; position: absolute; inset: -7px; border-radius: 50%;804 background: inherit; filter: blur(13px); opacity: 0.5;805 }806 .kx-spark-core { position: absolute; inset: 2px; border-radius: 50%; background: #0a0a0c; }807 .kx-spark-icon { position: absolute; inset: 0; margin: auto; width: 32px; height: 32px; color: #fff; }808 809 .kx-intro-title {810 font-family: var(--display);811 font-weight: 400;812 font-size: clamp(26px, 3vw, 34px);813 line-height: 1.12;814 color: #fff;815 margin-bottom: 16px;816 }817 .kx-intro-lead {818 font-size: 14.5px;819 font-weight: 300;820 line-height: 1.7;821 color: var(--t2);822 margin: 0 auto 26px;823 max-width: 48ch;824 }825 .kx-points {826 list-style: none;827 display: flex;828 flex-direction: column;829 gap: 13px;830 text-align: left;831 margin-bottom: 28px;832 }833 .kx-points li {834 position: relative;835 padding-left: 22px;836 font-size: 13.5px;837 line-height: 1.62;838 color: var(--t2);839 }840 .kx-points li::before {841 content: "";842 position: absolute; left: 2px; top: 0.6em;843 width: 6px; height: 6px; border-radius: 50%;844 }845 .kx-points li:nth-child(1)::before { background: #ff7a6b; box-shadow: 0 0 8px rgba(255, 122, 107, 0.7); }846 .kx-points li:nth-child(2)::before { background: #6fb3ff; box-shadow: 0 0 8px rgba(111, 179, 255, 0.7); }847 .kx-points li:nth-child(3)::before { background: #64ffa0; box-shadow: 0 0 8px rgba(100, 255, 160, 0.7); }848 .kx-points b { color: var(--t1); font-weight: 600; }849 .kx-intro-hint {850 font-family: var(--mono);851 font-size: 11px;852 letter-spacing: 0.08em;853 text-transform: uppercase;854 color: var(--t3);855 }856 857 .kx-code::-webkit-scrollbar, .kx-list::-webkit-scrollbar { width: 10px; height: 10px; }858 .kx-code::-webkit-scrollbar-thumb, .kx-list::-webkit-scrollbar-thumb { background: var(--line); border: 3px solid #0a0a0c; border-radius: 10px; }859 /* while the overlay is open, lock the page behind it so only the overlay's panes scroll */860 body.kx-locked { overflow: hidden; }861 @keyframes kxFade { from { opacity: 0; } to { opacity: 1; } }862 @keyframes kxRise { from { opacity: 0; transform: translateY(12px) scale(0.99); } to { opacity: 1; transform: none; } }863 @keyframes kxSpin { to { transform: rotate(360deg); } }864 @keyframes kxIntroRise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }865 866 @media (max-width: 760px) {867 .kx { padding: 0; }868 .kx-panel { width: 100%; height: 100%; border-radius: 0; border: 0; }869 .kx-body { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }870 .kx-side { border-right: 0; border-bottom: 1px solid var(--line-soft); }871 .kx-side::after { display: none; }872 .kx-list { flex-direction: row; height: auto; overflow-x: auto; overflow-y: hidden; }873 .kx-item { flex-shrink: 0; }874 }875 876 /* ====================================================================== */877 /* entrance + keyframes */878 /* ====================================================================== */879 880 .anim { opacity: 0; transition: opacity 0.9s cubic-bezier(0.25, 0.1, 0.25, 1); }881 .anim.in { opacity: 1; }882 .eyebrow { transition-delay: 0.35s; }883 .hero-h1 { transition-delay: 0.5s; }884 .hero-row { transition-delay: 0.78s; }885 886 @keyframes statusPulse {887 0%, 100% { opacity: 1; }888 50% { opacity: 0.3; }889 }890 @keyframes scrollDrop {891 0% { top: -100%; }892 55% { top: 100%; }893 100% { top: 100%; }894 }895 @keyframes rise {896 from { opacity: 0; transform: translateY(10px); }897 to { opacity: 1; transform: none; }898 }899 @keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }900 @keyframes bob {901 0%, 60%, 100% { opacity: 0.5; transform: translateY(0); }902 30% { opacity: 1; transform: translateY(-5px); }903 }904 905 @media (prefers-reduced-motion: reduce) {906 *, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }907 .anim, #crt-frame, .hero-fade, .scroll-cue { opacity: 1 !important; }908 .caret { opacity: 1; }909 }910 911 /* ====================================================================== */912 /* responsive */913 /* ====================================================================== */914 915 @media (max-width: 760px) {916 .hero-content { padding: 0 22px 22px; }917 .hero-h1 { font-size: clamp(38px, 12vw, 60px); margin-bottom: 24px; max-width: none; }918 .hero-row { flex-direction: column; align-items: stretch; gap: 28px; }919 .hero-side { align-items: flex-start; text-align: left; }920 .hero-foot { align-items: flex-start; }921 .hero-actions { flex-wrap: wrap; }922 .hero-stats { gap: 22px 30px; }923 .stat-val { font-size: 24px; }924 .scroll-cue { display: none; }925 .chat-head-inner { padding: 13px 18px; gap: 12px; }926 .to-top .brand-name { display: none; }927 .thread { padding: 28px 18px 20px; }928 .composer { padding: 0 18px 18px; }929 .bubble.user { max-width: 88%; }930 .head-btn { padding: 8px 12px; }931 }932</style>933</head>934<body>935 936 <!-- ======================= LANDING ======================= -->937 <section id="landing" class="screen">938 <div id="crt-frame"></div>939 <div class="hero-fade"></div>940 941 <div class="hero-overlay">942 <div class="hero-content">943 <div class="eyebrow anim">On-device · WebGPU · agentic kernel optimization</div>944 <h1 class="hero-h1 anim">Gemma 4 in your browser.<br><span class="thin">Kernels written by Fable 5.</span></h1>945 <div class="hero-row anim">946 <div class="hero-main">947 <p class="hero-sub">948 <b>Gemma 4 E2B (QAT Mobile)</b> — a powerful open-source model — runs949 fully on-device with WebGPU. Weights cache locally after the first load, and nothing you950 type ever leaves your machine.951 </p>952 <div class="hero-stats">953 <div class="stat"><span class="stat-val">2.3B</span><span class="stat-label">Effective params</span></div>954 <div class="stat"><span class="stat-val">128K</span><span class="stat-label">Context window</span></div>955 <div class="stat"><span class="stat-val">~250</span><span class="stat-label">tok/s · M4 Max</span></div>956 <div class="stat"><span class="stat-val">100%</span><span class="stat-label">On-device</span></div>957 </div>958 </div>959 <div class="hero-side">960 <div class="hero-actions">961 <button class="btn-primary" id="loadBtn" type="button">962 <span id="loadBtnLabel">Load model</span>963 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9M4 7l4 4 4-4"/></svg>964 </button>965 <a class="btn-secondary" href="https://huggingface.co/google/gemma-4-E2B-it-qat-mobile-transformers" target="_blank" rel="noopener">Model card</a>966 </div>967 <div class="hero-foot">968 <a class="kernel-note-cta" href="https://x.com/xenovacom/status/2065656427117437213" target="_blank" rel="noopener">WebGPU kernels <b>100% written & optimized by Fable 5</b> →</a>969 <span class="hero-foot-note">Tuned for Apple M4 Max · experimental</span>970 </div>971 </div>972 </div>973 </div>974 975 <div class="scroll-cue" id="scrollCue">976 <span>Chat below</span>977 <div class="scroll-cue-line"></div>978 </div>979 </div>980 </section>981 982 <!-- ======================= CHAT ======================= -->983 <section id="chat" class="screen">984 <header class="chat-head">985 <div class="chat-head-inner">986 <a class="to-top" href="#landing" id="toTop" title="Back to top">987 <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8 13V4M4 8l4-4 4 4"/></svg>988 <span class="brand-name">Gemma 4 · E2B</span>989 </a>990 <div class="status" id="status">991 <span class="status-dot"></span>992 <span class="status-text" id="statusText">Not loaded</span>993 </div>994 <div class="chat-head-actions">995 <button class="head-btn solid" id="headLoadBtn" type="button">Load model</button>996 <button class="head-btn" id="kernelsBtn" type="button" hidden>View Kernels</button>997 <button class="head-btn" id="clearBtn" type="button" disabled>Clear</button>998 </div>999 </div>1000 <div class="bar" id="bar"><div></div></div>1001 </header>1002 1003 <div class="thread-scroll" id="threadScroll">1004 <div class="thread" id="thread">1005 <div class="welcome" id="welcome">1006 <h2>What's on your <span class="thin">mind today?</span></h2>1007 <p>Model runs entirely on your device.</p>1008 <div class="seeds">1009 <button class="seed" type="button" disabled>How does WebGPU differ from WebGL?</button>1010 <button class="seed" type="button" disabled>Write a haiku about on-device AI</button>1011 <button class="seed" type="button" disabled>What is quantization-aware training?</button>1012 </div>1013 </div>1014 </div>1015 </div>1016 1017 <footer class="composer-wrap">1018 <div class="composer">1019 <div class="field">1020 <textarea id="input" rows="1" placeholder="Load the model to start chatting..." disabled></textarea>1021 <button class="icon-button send-button" id="sendBtn" type="button" disabled aria-label="Send message">1022 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>1023 </button>1024 <button class="icon-button stop-button" id="stopBtn" type="button" aria-label="Stop generation">1025 <svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg>1026 </button>1027 </div>1028 <div class="composer-meta">1029 <span id="hint">Runs fully on-device — nothing leaves your machine</span>1030 <span id="liveStat"></span>1031 </div>1032 </div>1033 </footer>1034 </section>1035 1036 <!-- ============ KERNELS OVERLAY ============ -->1037 <div class="kx" id="kernelsOverlay" hidden>1038 <div class="kx-backdrop" data-close></div>1039 <div class="kx-panel" role="dialog" aria-modal="true" aria-label="Rendered kernels">1040 <div class="kx-head">1041 <div class="kx-title">1042 <h3>Kernels</h3>1043 <span class="kx-sub" id="kxSub"></span>1044 </div>1045 <button class="kx-close" id="kxClose" type="button" aria-label="Close" data-close>1046 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>1047 </button>1048 </div>1049 <div class="kx-body">1050 <div class="kx-side"><div class="kx-list" id="kxList"></div></div>1051 <div class="kx-view">1052 <div class="kx-intro" id="kxIntro">1053 <div class="kx-intro-inner">1054 <div class="kx-spark" aria-hidden="true">1055 <span class="kx-spark-ring"></span>1056 <span class="kx-spark-core"></span>1057 <svg class="kx-spark-icon" viewBox="0 0 20 20" fill="currentColor"><path d="M11.7858 1.66699C11.9437 1.66699 12.0957 1.72951 12.2074 1.84115C12.3189 1.95276 12.3815 2.10483 12.3815 2.2627V3.45247H15.3568C15.6725 3.45247 15.9767 3.57757 16.1999 3.80078C16.4231 4.02403 16.5482 4.32813 16.5482 4.64388V7.61914H17.738C17.8957 7.61914 18.0479 7.68181 18.1595 7.79329C18.2712 7.90491 18.3337 8.05698 18.3337 8.21484C18.3336 8.37247 18.2709 8.52323 18.1595 8.63477C18.0478 8.7464 17.8958 8.81055 17.738 8.81055H16.5482V11.1901H17.738C17.8958 11.1901 18.0478 11.2543 18.1595 11.3659C18.2709 11.4774 18.3336 11.6282 18.3337 11.7858C18.3337 11.9437 18.2712 12.0957 18.1595 12.2074C18.0479 12.3188 17.8957 12.3815 17.738 12.3815H16.5482V15.3568C16.5482 15.6725 16.4231 15.9767 16.1999 16.1999C15.9767 16.4231 15.6725 16.5482 15.3568 16.5482H12.3815V17.738C12.3815 17.8959 12.3188 18.0479 12.2074 18.1595C12.0957 18.271 11.9437 18.3337 11.7858 18.3337C11.6282 18.3336 11.4774 18.2707 11.3659 18.1595C11.2543 18.0478 11.1901 17.896 11.1901 17.738V16.5482H8.81055V17.738C8.81055 17.896 8.7464 18.0478 8.63477 18.1595C8.52323 18.2707 8.37247 18.3336 8.21484 18.3337C8.05698 18.3337 7.90491 18.271 7.79329 18.1595C7.68181 18.0479 7.61914 17.8958 7.61914 17.738V16.5482H4.64388C4.32813 16.5482 4.02403 16.4231 3.80078 16.1999C3.57756 15.9767 3.45247 15.6725 3.45247 15.3568V12.3815H2.2627C2.10485 12.3815 1.95276 12.3189 1.84115 12.2074C1.72951 12.0957 1.66699 11.9437 1.66699 11.7858C1.66708 11.6283 1.72987 11.4774 1.84115 11.3659C1.95276 11.2543 2.10483 11.1901 2.2627 11.1901H3.45247V8.81055H2.2627C2.10483 8.81055 1.95276 8.7464 1.84115 8.63477C1.72987 8.52324 1.66704 8.37243 1.66699 8.21484C1.66699 8.05698 1.72951 7.90491 1.84115 7.79329C1.95275 7.68175 2.1049 7.61914 2.2627 7.61914H3.45247V4.64388C3.45247 4.32813 3.57751 4.02403 3.80078 3.80078C4.02403 3.57753 4.32813 3.45247 4.64388 3.45247H7.61914V2.2627C7.61914 2.10488 7.68175 1.95275 7.79329 1.84115C7.90491 1.72951 8.05698 1.66699 8.21484 1.66699C8.37243 1.66704 8.52324 1.72987 8.63477 1.84115C8.7464 1.95276 8.81055 2.10481 8.81055 2.2627V3.45247H11.1901V2.2627C11.1901 2.10481 11.2543 1.95276 11.3659 1.84115C11.4774 1.72987 11.6283 1.66708 11.7858 1.66699ZM6.14616 5.31445C5.6863 5.31489 5.31445 5.68782 5.31445 6.14779V13.8529C5.31445 14.3131 5.68755 14.6862 6.14779 14.6862H13.8529C14.3131 14.6862 14.6862 14.3131 14.6862 13.8529V6.14779C14.6862 5.68755 14.3131 5.31445 13.8529 5.31445H6.14616Z" fill-opacity="0.4"></path><rect x="5.31348" y="5.31445" width="9.37256" height="9.37256" rx="0.833333" fill-opacity="0.15"></rect><path d="M8.19238 7.91225V12.254C8.19238 12.585 8.55699 12.7862 8.83777 12.606L12.2491 10.4351C12.3088 10.3973 12.358 10.3451 12.3921 10.2831C12.4262 10.2212 12.4441 10.1517 12.4441 10.081C12.4441 10.0103 12.4262 9.9408 12.3921 9.87889C12.358 9.81697 12.3088 9.76468 12.2491 9.72688L8.83777 7.56022C8.77456 7.51934 8.70149 7.49627 8.62626 7.49346C8.55103 7.49064 8.47644 7.50819 8.41035 7.54423C8.34426 7.58028 8.28913 7.6335 8.25077 7.69827C8.2124 7.76304 8.19223 7.83697 8.19238 7.91225Z" fill-opacity="0.8"></path></svg>1058 </div>1059 <h2 class="kx-intro-title">What are Kernels?</h2>1060 <p class="kx-intro-lead">Kernels are the low-level GPU programs that do the model's actual math — the matrix multiplications, attention, and normalization behind every token. And how well they're optimized can dramatically speed up inference.</p>1061 <ul class="kx-points">1062 <li><b>WebGPU & WGSL.</b> Each kernel is a WebGPU compute shader, written in WGSL — the language that runs general-purpose math on the GPU — entirely locally in your browser.</li>1063 <li><b>Agentic Kernel Optimization.</b> Every kernel was generated by AI (in this case, Fable 5, before it was shut down), benchmarked on an Apple M4 Max, and refined through an evolutionary, genetic-style search toward the fastest version.</li>1064 <li><b>Blazingly Fast.</b> This means we are able to run Gemma 4 E2B at ~250 tokens/sec on an M4 Max, pushing your device to its limits.</li>1065 </ul>1066 <div class="kx-intro-hint">Select a kernel to read its real source</div>1067 </div>1068 </div>1069 <div class="kx-source" id="kxSource" hidden>1070 <div class="kx-view-head">1071 <span class="kx-name" id="kxName"></span>1072 <div class="kx-view-actions">1073 <span class="kx-lines" id="kxLines"></span>1074 <button class="kx-copy" id="kxCopy" type="button">Copy</button>1075 </div>1076 </div>1077 <pre class="kx-code"><code id="kxCode"></code></pre>1078 </div>1079 </div>1080 </div>1081 </div>1082 </div>1083 1084 <!-- landing background animation (WebGL / three.js) -->1085 <script type="module" src="./landing.js"></script>1086 1087 <!-- entrance sequencer (runs independently of the background so the hero shows even if1088 the WebGL/three.js scene fails to load) -->1089 <script>1090 (function () {1091 const fire = () => {1092 document.getElementById("crt-frame")?.classList.add("visible");1093 document.querySelector(".hero-fade")?.classList.add("in");1094 document.querySelectorAll(".anim").forEach((el) => el.classList.add("in"));1095 document.getElementById("scrollCue")?.classList.add("in");1096 };1097 requestAnimationFrame(() => requestAnimationFrame(fire));1098 })();1099 </script>1100 1101 <!-- app logic -->1102 <script type="module">1103 import { Gemma4Mobile } from "./gemma-4-e2b.js";1104 1105 // Streamed markdown → HTML. `streamdown` itself is React-only (peer deps react/react-dom plus a1106 // mermaid + unified/remark/rehype dep tree), so we use `marked` — the very parser streamdown is1107 // built on — loaded lazily from a CDN. Until it loads (or if the CDN is unreachable) we fall1108 // back to the tiny inline renderer below, so chat never depends on it.1109 let marked = null;1110 import("https://esm.sh/marked@17")1111 .then((m) => { marked = m.marked; marked.use({ gfm: true, breaks: true }); })1112 .catch(() => { marked = null; });1113 1114 const $ = (id) => document.getElementById(id);1115 const landing = $("landing");1116 const chat = $("chat");1117 const threadScroll = $("threadScroll");1118 const thread = $("thread");1119 const loadBtn = $("loadBtn");1120 const loadBtnLabel = $("loadBtnLabel");1121 const headLoadBtn = $("headLoadBtn");1122 const statusEl = $("status");1123 const statusText = $("statusText");1124 const bar = $("bar");1125 const barFill = bar.firstElementChild;1126 const input = $("input");1127 const sendBtn = $("sendBtn");1128 const stopBtn = $("stopBtn");1129 const clearBtn = $("clearBtn");1130 const hint = $("hint");1131 const liveStat = $("liveStat");1132 const kernelsBtn = $("kernelsBtn");1133 const kernelsOverlay = $("kernelsOverlay");1134 1135 let model = null;1136 let kernels = [];1137 let kxCopySource = "";1138 let messages = [];1139 let abortController = null;1140 let isGenerating = false;1141 let isLoading = false;1142 let renderScheduled = false;1143 let renderState = null;1144 let targetProgress = 0; // latest monotonic load fraction [0,1]1145 let shownProgress = 0; // currently rendered bar fraction (eased toward target each frame)1146 let progressRaf = 0;1147 1148 if (!navigator.gpu) {1149 const msg = "WebGPU isn't available here. Try a recent Chrome, Edge, or Safari Technology Preview.";1150 loadBtn.disabled = true;1151 headLoadBtn.disabled = true;1152 setStatus("error", msg);1153 }1154 1155 loadBtn.addEventListener("click", loadModel);1156 headLoadBtn.addEventListener("click", loadModel);1157 sendBtn.addEventListener("click", send);1158 stopBtn.addEventListener("click", () => abortController?.abort());1159 clearBtn.addEventListener("click", clearChat);1160 kernelsBtn.addEventListener("click", openKernels);1161 kernelsOverlay.addEventListener("click", (e) => { if (e.target.closest("[data-close]")) closeKernels(); });1162 $("kxList").addEventListener("scroll", updateListFade, { passive: true });1163 $("kxCopy").addEventListener("click", copyKernel);1164 document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !kernelsOverlay.hidden) closeKernels(); });1165 $("toTop").addEventListener("click", (e) => { e.preventDefault(); landing.scrollIntoView({ behavior: "smooth" }); });1166 1167 input.addEventListener("input", () => { autoGrow(); refreshSend(); });1168 input.addEventListener("keydown", (e) => {1169 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (!sendBtn.disabled) send(); }1170 });1171 document.addEventListener("click", (e) => {1172 const seed = e.target.closest(".seed");1173 if (!seed || seed.disabled || !model || isGenerating) return;1174 input.value = seed.textContent;1175 send();1176 });1177 1178 function setStatus(state, text) {1179 statusEl.className = "status" + (state ? " " + state : "");1180 if (text !== undefined) statusText.innerHTML = text;1181 }1182 1183 async function loadModel() {1184 if (model || isLoading) return;1185 isLoading = true;1186 loadBtn.disabled = true;1187 headLoadBtn.disabled = true;1188 loadBtnLabel.textContent = "Loading…";1189 bar.classList.remove("done");1190 setProgressImmediate(0.02);1191 setStatus("loading", "Requesting WebGPU device…");1192 1193 // Move the visitor down into the chat where progress + conversation live.1194 chat.scrollIntoView({ behavior: "smooth" });1195 1196 const started = performance.now();1197 try {1198 model = await Gemma4Mobile.load(null, { onProgress: updateLoadProgress });1199 setStatus("loading", "Warming up kernels…");1200 await model.warmup();