Team Ai
Apppublic

Keyonll/gemma-4-webgpu-kernels

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
index.html1620 linesDownload Raw Back to root
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&nbsp;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&nbsp;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 &amp; optimized by Fable&nbsp;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 &amp; 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();

Showing the first 1,200 of 1620 lines. Download the file for the rest.