Spaces:
Running on Zero
Running on Zero
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>💎 Gemma 4 Playground</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Cabinet+Grotesk:wght@300;400;500;600;700;800&family=Geist+Mono:wght@300;400;500&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box;} | |
| :root{ | |
| --cream:#faf8f5;--paper:#f5f2ed;--white:#ffffff;--fog:#ede9e3; | |
| --line:#e4dfd8;--line2:#d5cfc7; | |
| --ink:#1c1917;--ink2:#44403c;--ink3:#78716c;--ink4:#a8a29e; | |
| --v:#6d28d9;--v2:#7c3aed;--vg:rgba(109,40,217,.08); | |
| --teal:#0d9488;--tg:rgba(13,148,136,.07); | |
| --rose:#e11d48;--green:#16a34a;--amber:#d97706; | |
| --r:20px;--r-sm:12px;--r-xs:8px; | |
| --sh:0 2px 12px rgba(28,25,23,.06),0 1px 3px rgba(28,25,23,.04); | |
| --sh2:0 8px 32px rgba(28,25,23,.08),0 2px 8px rgba(28,25,23,.04); | |
| --sh3:0 20px 60px rgba(28,25,23,.10),0 4px 16px rgba(28,25,23,.06); | |
| --fd:'Instrument Serif',serif; | |
| --fb:'Cabinet Grotesk',sans-serif; | |
| --fm:'Geist Mono',monospace; | |
| } | |
| html,body{height:100%;overflow:hidden;background:var(--cream);color:var(--ink);font-family:var(--fb);-webkit-font-smoothing:antialiased;} | |
| .bg{position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;} | |
| .orb{position:absolute;border-radius:50%;filter:blur(80px);opacity:.5;animation:drift linear infinite;} | |
| .orb1{width:500px;height:500px;background:radial-gradient(circle,rgba(109,40,217,.35),transparent 70%);top:-100px;left:-100px;animation-duration:22s;} | |
| .orb2{width:380px;height:380px;background:radial-gradient(circle,rgba(16,185,129,.28),transparent 70%);top:5%;right:-60px;animation-duration:28s;animation-delay:-9s;} | |
| .orb3{width:320px;height:320px;background:radial-gradient(circle,rgba(252,211,77,.22),transparent 70%);bottom:-60px;left:35%;animation-duration:19s;animation-delay:-5s;} | |
| @keyframes drift{0%{transform:translate(0,0) scale(1);}33%{transform:translate(40px,-30px) scale(1.05);}66%{transform:translate(-20px,20px) scale(.97);}100%{transform:translate(0,0) scale(1);}} | |
| .bg-grid{position:absolute;inset:0;background-image:radial-gradient(circle,rgba(28,25,23,.06) 1px,transparent 1px);background-size:28px 28px;} | |
| .shell{position:relative;z-index:1;display:grid;grid-template-columns:280px 1fr;height:100vh;} | |
| /* SIDEBAR */ | |
| .sidebar{display:flex;flex-direction:column;background:rgba(255,255,255,.75);backdrop-filter:blur(24px);border-right:1px solid var(--line);overflow:hidden;} | |
| .logo-area{padding:20px 18px 14px;border-bottom:1px solid var(--line);} | |
| .logo-row{display:flex;align-items:center;gap:10px;margin-bottom:8px;} | |
| .logo-icon{width:42px;height:42px;border-radius:13px;flex-shrink:0;background:linear-gradient(135deg,#6d28d9,#a78bfa,#10b981);display:flex;align-items:center;justify-content:center;font-size:22px;box-shadow:0 4px 14px rgba(109,40,217,.3);} | |
| .logo-text{line-height:1.2;} | |
| .logo-name{font-family:var(--fd);font-size:20px;color:var(--ink);} | |
| .logo-name em{color:var(--v);font-style:italic;} | |
| .logo-sub{font-size:9px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:var(--ink4);} | |
| .model-card{margin:14px;padding:14px 16px;border-radius:var(--r-sm);border:1.5px solid rgba(109,40,217,.25);background:linear-gradient(135deg,rgba(109,40,217,.04),rgba(16,185,129,.03));box-shadow:var(--sh);} | |
| .mc-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;} | |
| .mc-name{font-size:13px;font-weight:700;color:var(--ink);} | |
| .mc-arch{font-size:9px;font-weight:700;letter-spacing:.4px;padding:2px 8px;border-radius:10px;background:var(--vg);color:var(--v);font-family:var(--fm);} | |
| .mc-stats{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;} | |
| .mc-stat{font-size:9px;font-weight:600;padding:2px 7px;border-radius:6px;font-family:var(--fm);} | |
| .mc-ok{background:rgba(22,163,74,.09);color:#16a34a;} | |
| .mc-hl{background:rgba(109,40,217,.08);color:var(--v);} | |
| .mc-desc{font-size:10px;color:var(--ink3);line-height:1.6;} | |
| .settings{padding:14px;flex:1;overflow-y:auto;} | |
| .settings::-webkit-scrollbar{width:3px;} | |
| .settings::-webkit-scrollbar-thumb{background:var(--line2);border-radius:10px;} | |
| .field{margin-bottom:12px;} | |
| .field-lbl{font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--ink4);margin-bottom:6px;display:block;} | |
| .field textarea{width:100%;background:var(--fog);border:1.5px solid var(--line);border-radius:var(--r-xs);color:var(--ink);font-family:var(--fb);font-size:12px;line-height:1.6;padding:8px 10px;resize:none;outline:none;transition:border-color .2s,box-shadow .2s;height:64px;} | |
| .field textarea:focus{border-color:rgba(109,40,217,.35);box-shadow:0 0 0 3px rgba(109,40,217,.08);} | |
| .toggle-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;} | |
| .toggle-info{flex:1;} | |
| .tl{font-size:11px;font-weight:600;color:var(--ink2);} | |
| .ts{font-size:9px;color:var(--ink4);margin-top:1px;} | |
| .toggle{position:relative;width:36px;height:20px;flex-shrink:0;} | |
| .toggle input{opacity:0;width:0;height:0;position:absolute;} | |
| .t-track{position:absolute;inset:0;border-radius:10px;background:var(--line2);cursor:pointer;transition:background .22s;} | |
| .toggle input:checked+.t-track{background:linear-gradient(135deg,var(--v),var(--v2));} | |
| .t-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--white);box-shadow:0 1px 4px rgba(0,0,0,.15);transition:transform .22s cubic-bezier(.4,0,.2,1);} | |
| .toggle input:checked~.t-thumb{transform:translateX(16px);} | |
| .presets{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px;} | |
| .chip{font-size:9px;font-weight:600;padding:3px 9px;border-radius:20px;background:var(--fog);border:1px solid var(--line);color:var(--ink3);cursor:pointer;transition:all .2s;white-space:nowrap;} | |
| .chip:hover{background:rgba(109,40,217,.08);border-color:rgba(109,40,217,.25);color:var(--v);} | |
| .sl-wrap{margin-bottom:10px;} | |
| .sl-meta{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;} | |
| .sl-title{font-size:11px;font-weight:600;color:var(--ink2);} | |
| .sl-val{font-size:10px;font-weight:700;font-family:var(--fm);background:var(--fog);color:var(--ink3);padding:1px 6px;border-radius:5px;border:1px solid var(--line);} | |
| input[type=range]{width:100%;height:4px;appearance:none;background:var(--line);border-radius:2px;outline:none;cursor:pointer;} | |
| input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:15px;height:15px;border-radius:50%;background:linear-gradient(135deg,var(--v),var(--v2));box-shadow:0 2px 6px rgba(109,40,217,.3);border:2px solid var(--white);cursor:pointer;transition:transform .2s;} | |
| input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.2);} | |
| .clear-btn{width:100%;padding:8px;border-radius:var(--r-xs);background:var(--fog);border:1.5px solid var(--line);color:var(--ink3);font-family:var(--fb);font-size:11px;font-weight:600;cursor:pointer;transition:all .2s;margin-top:4px;} | |
| .clear-btn:hover{border-color:rgba(225,29,72,.3);color:var(--rose);} | |
| .section-lbl{font-size:9px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:var(--ink4);margin-bottom:10px;padding:0 2px;} | |
| /* MAIN CHAT */ | |
| .chat-main{display:flex;flex-direction:column;background:rgba(250,248,245,.5);overflow:hidden;} | |
| .chat-hdr{padding:14px 22px;border-bottom:1px solid var(--line);background:rgba(255,255,255,.82);backdrop-filter:blur(20px);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;} | |
| .hdr-left{display:flex;align-items:center;gap:10px;} | |
| .model-pill{display:flex;align-items:center;gap:7px;padding:5px 13px;border-radius:30px;background:linear-gradient(135deg,rgba(109,40,217,.08),rgba(16,185,129,.06));border:1px solid rgba(109,40,217,.2);} | |
| .dot{width:7px;height:7px;border-radius:50%;background:linear-gradient(135deg,var(--v),#10b981);animation:pulse 2s ease-in-out infinite;} | |
| @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(109,40,217,.4);}50%{box-shadow:0 0 0 4px rgba(109,40,217,0);}} | |
| .model-name{font-size:12px;font-weight:700;color:var(--v);font-family:var(--fm);} | |
| .mode-tag{font-size:10px;font-weight:600;padding:4px 10px;border-radius:20px;background:var(--fog);border:1px solid var(--line);color:var(--ink3);font-family:var(--fm);transition:all .3s;} | |
| .mode-tag.thinking{background:rgba(13,148,136,.08);border-color:rgba(13,148,136,.25);color:var(--teal);} | |
| .hdr-stats{display:flex;gap:16px;} | |
| .hstat{text-align:center;} | |
| .hstat-n{font-size:17px;font-weight:800;color:var(--ink);font-family:var(--fd);} | |
| .hstat-l{font-size:9px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;color:var(--ink4);} | |
| .hf-login-btn{display:inline-flex;align-items:center;gap:8px;padding:7px 14px;border-radius:22px;border:1.5px solid rgba(109,40,217,.25);background:linear-gradient(135deg,rgba(109,40,217,.1),rgba(139,92,246,.08));color:var(--v);font-family:var(--fb);font-size:12px;font-weight:700;cursor:pointer;text-decoration:none;transition:all .22s;white-space:nowrap;} | |
| .hf-login-btn:hover{background:linear-gradient(135deg,rgba(109,40,217,.18),rgba(139,92,246,.14));border-color:rgba(109,40,217,.45);box-shadow:0 4px 14px rgba(109,40,217,.18);transform:translateY(-1px);} | |
| .user-chip{display:inline-flex;align-items:center;gap:8px;padding:4px 12px 4px 5px;border-radius:22px;border:1.5px solid var(--line);background:rgba(255,255,255,.8);font-size:12px;font-weight:600;color:var(--ink2);cursor:pointer;text-decoration:none;transition:all .2s;} | |
| .user-chip:hover{border-color:rgba(109,40,217,.3);box-shadow:var(--sh);} | |
| .user-avatar{width:24px;height:24px;border-radius:50%;background:linear-gradient(135deg,var(--v),#a78bfa);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;overflow:hidden;flex-shrink:0;} | |
| .user-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%;} | |
| .logout-btn{padding:5px 11px;border-radius:18px;border:1.5px solid var(--line);background:var(--fog);color:var(--ink4);font-size:11px;font-weight:600;cursor:pointer;transition:all .2s;font-family:var(--fb);} | |
| .logout-btn:hover{border-color:rgba(225,29,72,.3);color:var(--rose);} | |
| .messages{flex:1;overflow-y:auto;padding:22px 40px;display:flex;flex-direction:column;gap:18px;scroll-behavior:smooth;} | |
| .messages::-webkit-scrollbar{width:4px;} | |
| .messages::-webkit-scrollbar-thumb{background:var(--line2);border-radius:10px;} | |
| .welcome{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:30px 36px;gap:20px;flex:1;min-height:400px;} | |
| .welcome-icon{width:72px;height:72px;border-radius:20px;background:linear-gradient(135deg,#6d28d9,#a78bfa,#10b981);display:flex;align-items:center;justify-content:center;font-size:34px;box-shadow:0 8px 30px rgba(109,40,217,.25);animation:float 4s ease-in-out infinite;} | |
| @keyframes float{0%,100%{transform:translateY(0);}50%{transform:translateY(-8px);}} | |
| .welcome-title{font-family:var(--fd);font-size:34px;color:var(--ink);line-height:1.2;} | |
| .welcome-title em{font-style:italic;color:var(--v);} | |
| .welcome-sub{font-size:13px;color:var(--ink3);line-height:1.7;max-width:450px;} | |
| .welcome-badges{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;} | |
| .welcome-badge{font-size:11px;font-weight:600;padding:4px 12px;border-radius:20px;font-family:var(--fm);} | |
| .wb-purple{background:rgba(109,40,217,.08);color:var(--v);border:1px solid rgba(109,40,217,.15);} | |
| .wb-green{background:rgba(16,185,129,.08);color:#059669;border:1px solid rgba(16,185,129,.15);} | |
| .wb-amber{background:rgba(217,119,6,.08);color:var(--amber);border:1px solid rgba(217,119,6,.15);} | |
| .ex-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:9px;width:100%;max-width:500px;} | |
| .ex-card{background:var(--white);border:1.5px solid var(--line);border-radius:var(--r-sm);padding:12px 14px;cursor:pointer;text-align:left;transition:all .22s;} | |
| .ex-card:hover{border-color:rgba(109,40,217,.3);box-shadow:var(--sh);transform:translateY(-2px);} | |
| .ex-icon{font-size:16px;margin-bottom:5px;display:block;} | |
| .ex-title{font-size:11px;font-weight:700;color:var(--ink);margin-bottom:2px;} | |
| .ex-desc{font-size:10px;color:var(--ink3);line-height:1.5;} | |
| .msg{display:flex;gap:10px;animation:msgIn .32s cubic-bezier(.34,1.56,.64,1) both;} | |
| @keyframes msgIn{from{opacity:0;transform:translateY(10px) scale(.97);}to{opacity:1;transform:none;}} | |
| .msg.user{flex-direction:row-reverse;} | |
| .avatar{width:30px;height:30px;border-radius:9px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px;margin-top:2px;} | |
| .msg.user .avatar{background:linear-gradient(135deg,var(--v),#a78bfa);color:#fff;} | |
| .msg.bot .avatar{background:linear-gradient(135deg,#f0fdf4,#dcfce7);border:1px solid #d1fae5;font-size:15px;} | |
| .body{max-width:82%;display:flex;flex-direction:column;gap:3px;} | |
| .msg.user .body{align-items:flex-end;} | |
| .bubble{padding:11px 15px;border-radius:17px;font-size:13.5px;line-height:1.7;color:var(--ink);} | |
| .msg.user .bubble{background:linear-gradient(135deg,#6d28d9,#7c3aed);color:#fff;border-radius:17px 3px 17px 17px;box-shadow:0 4px 16px rgba(109,40,217,.22);} | |
| .msg.bot .bubble{background:var(--white);border:1px solid var(--line);border-radius:3px 17px 17px 17px;box-shadow:var(--sh);margin-left:8px;padding:16px 22px;} | |
| .msg-time{font-size:9px;color:var(--ink4);padding:0 3px;font-family:var(--fm);} | |
| .think-blk{background:linear-gradient(135deg,rgba(13,148,136,.04),rgba(6,182,212,.04));border:1px solid rgba(13,148,136,.2);border-radius:9px;padding:8px 12px;margin-bottom:10px;} | |
| .think-hdr{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:10px;font-weight:700;color:var(--teal);letter-spacing:.4px;text-transform:uppercase;user-select:none;} | |
| .think-hdr::before{content:'▶';font-size:8px;transition:transform .2s;} | |
| .think-blk.open .think-hdr::before{content:'▼';} | |
| .think-body{margin-top:7px;display:none;border-top:1px solid rgba(13,148,136,.15);padding-top:7px;font-size:12px;color:var(--ink3);line-height:1.6;} | |
| .think-blk.open .think-body{display:block;} | |
| .bubble pre{background:#0d1117;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:10px 12px;margin:7px 0;overflow-x:auto;font-family:var(--fm);font-size:11.5px;line-height:1.6;position:relative;} | |
| .bubble code{font-family:var(--fm);font-size:11.5px;background:var(--fog);padding:1px 5px;border-radius:4px;} | |
| .bubble pre code{background:transparent;padding:0;} | |
| /* Code copy button */ | |
| .code-wrap{position:relative;margin:7px 0;} | |
| .code-wrap pre{margin:0;} | |
| .code-header{display:flex;justify-content:space-between;align-items:center;padding:6px 14px;background:#161b22;border-radius:8px 8px 0 0;border:1px solid rgba(255,255,255,.08);border-bottom:none;} | |
| .code-lang-tag{font-size:11px;font-weight:600;color:#8b949e;font-family:var(--fm);} | |
| .copy-btn{padding:3px 10px;border-radius:6px;border:1px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:#8b949e;font-size:10px;font-weight:600;cursor:pointer;font-family:var(--fm);transition:all .2s;} | |
| .copy-btn:hover{background:rgba(255,255,255,.12);color:#c9d1d9;} | |
| .copy-btn.copied{background:rgba(22,163,74,.15);color:#16a34a;border-color:rgba(22,163,74,.3);} | |
| .code-wrap pre{border-radius:0 0 8px 8px;} | |
| /* Response speed badge */ | |
| .speed-badge{display:inline-flex;align-items:center;gap:5px;margin-top:4px;padding:2px 8px;border-radius:12px;background:rgba(109,40,217,.06);border:1px solid rgba(109,40,217,.12);font-size:9px;font-weight:600;font-family:var(--fm);color:var(--v);} | |
| /* Reasoning depth badge */ | |
| .reason-badge{display:inline-flex;align-items:center;gap:4px;margin-left:6px;padding:2px 8px;border-radius:12px;background:rgba(13,148,136,.06);border:1px solid rgba(13,148,136,.15);font-size:9px;font-weight:600;font-family:var(--fm);color:var(--teal);} | |
| /* Search bar */ | |
| .search-bar{display:none;padding:8px 18px 0;} | |
| .search-bar.show{display:block;} | |
| .search-wrap{display:flex;gap:6px;align-items:center;} | |
| .search-input{flex:1;padding:7px 12px;border-radius:var(--r-xs);border:1.5px solid var(--line);background:var(--fog);font-family:var(--fb);font-size:12px;color:var(--ink);outline:none;transition:border-color .2s;} | |
| .search-input:focus{border-color:rgba(109,40,217,.35);} | |
| .search-go{padding:7px 14px;border-radius:var(--r-xs);border:none;background:linear-gradient(135deg,#6d28d9,#7c3aed);color:#fff;font-size:11px;font-weight:700;cursor:pointer;font-family:var(--fb);} | |
| .search-results{margin-top:6px;padding:8px 10px;background:var(--white);border:1px solid var(--line);border-radius:var(--r-xs);font-size:11px;max-height:150px;overflow-y:auto;display:none;} | |
| .search-results.show{display:block;} | |
| .sr-item{padding:4px 0;border-bottom:1px solid var(--fog);} | |
| .sr-item:last-child{border:none;} | |
| .sr-title{font-weight:700;color:var(--ink);font-size:11px;} | |
| .sr-desc{color:var(--ink3);font-size:10px;line-height:1.4;} | |
| .sr-url{color:var(--v);font-size:9px;text-decoration:none;} | |
| /* PDF indicator */ | |
| .pdf-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:6px;background:rgba(225,29,72,.06);border:1px solid rgba(225,29,72,.15);font-size:10px;font-weight:600;color:var(--rose);} | |
| .typing{display:flex;align-items:center;gap:5px;padding:12px 15px;} | |
| .typing span{width:6px;height:6px;border-radius:50%;background:var(--v2);opacity:.4;animation:bounce .8s ease-in-out infinite;} | |
| .typing span:nth-child(2){animation-delay:.15s;} | |
| .typing span:nth-child(3){animation-delay:.3s;} | |
| @keyframes bounce{0%,100%{transform:translateY(0);opacity:.4;}50%{transform:translateY(-5px);opacity:1;}} | |
| .img-prev{display:none;padding:8px 26px 0;} | |
| .img-prev.show{display:block;} | |
| .img-tw{display:inline-flex;align-items:center;gap:8px;background:var(--white);border:1.5px solid rgba(109,40,217,.2);border-radius:var(--r-xs);padding:5px 9px 5px 5px;} | |
| .img-th{width:44px;height:44px;object-fit:cover;border-radius:5px;} | |
| .img-nm{font-size:10px;font-weight:600;color:var(--ink2);} | |
| .img-sz{font-size:9px;color:var(--ink4);} | |
| .img-rm{width:17px;height:17px;border-radius:50%;background:var(--fog);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:9px;color:var(--ink3);transition:all .2s;margin-left:4px;} | |
| .img-rm:hover{background:rgba(225,29,72,.1);color:var(--rose);} | |
| .inputbar{padding:14px 18px 16px;border-top:1px solid var(--line);background:rgba(255,255,255,.88);backdrop-filter:blur(20px);flex-shrink:0;} | |
| .input-wrap{display:flex;align-items:flex-end;gap:8px;background:var(--white);border:1.5px solid var(--line2);border-radius:var(--r);padding:7px 9px 7px 13px;transition:border-color .22s,box-shadow .22s;box-shadow:var(--sh);} | |
| .input-wrap:focus-within{border-color:rgba(109,40,217,.32);box-shadow:var(--sh2),0 0 0 4px rgba(109,40,217,.06);} | |
| .chat-ta{flex:1;border:none;outline:none;background:transparent;font-family:var(--fb);font-size:13.5px;color:var(--ink);line-height:1.6;resize:none;max-height:150px;min-height:22px;overflow-y:auto;padding:3px 0;} | |
| .chat-ta::placeholder{color:var(--ink4);} | |
| .input-acts{display:flex;align-items:center;gap:5px;padding-bottom:3px;} | |
| .icon-btn{width:32px;height:32px;border-radius:9px;border:1.5px solid var(--line);background:var(--fog);display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--ink3);font-size:14px;transition:all .2s;flex-shrink:0;} | |
| .icon-btn:hover{background:rgba(109,40,217,.08);border-color:rgba(109,40,217,.25);color:var(--v);} | |
| .file-btn{position:relative;} | |
| .file-btn input{position:absolute;inset:0;opacity:0;cursor:pointer;font-size:0;} | |
| .send-btn{width:36px;height:36px;border-radius:11px;border:none;background:linear-gradient(135deg,#6d28d9,#7c3aed);color:#fff;font-size:15px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 14px rgba(109,40,217,.28);transition:all .22s cubic-bezier(.4,0,.2,1);} | |
| .send-btn:hover{transform:translateY(-2px) scale(1.06);box-shadow:0 6px 20px rgba(109,40,217,.38);} | |
| .send-btn:active{transform:scale(.96);} | |
| .send-btn:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none;} | |
| .input-hint{font-size:10px;color:var(--ink4);padding:5px 3px 0;display:flex;align-items:center;gap:8px;} | |
| .kbd{background:var(--fog);border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-family:var(--fm);font-size:9px;color:var(--ink3);} | |
| .model-hint{margin-left:auto;font-weight:700;color:var(--v);font-size:10px;} | |
| .toast{position:fixed;bottom:20px;right:20px;z-index:9999;background:var(--ink);color:#fff;font-size:11px;font-weight:600;padding:9px 16px;border-radius:var(--r-sm);box-shadow:var(--sh3);transform:translateY(50px);opacity:0;transition:all .35s cubic-bezier(.34,1.56,.64,1);pointer-events:none;} | |
| .toast.show{transform:translateY(0);opacity:1;} | |
| @media(max-width:768px){ | |
| .shell{grid-template-columns:1fr;} | |
| .sidebar{display:none;} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg"> | |
| <div class="orb orb1"></div><div class="orb orb2"></div><div class="orb orb3"></div> | |
| <div class="bg-grid"></div> | |
| </div> | |
| <div class="shell"> | |
| <!-- SIDEBAR --> | |
| <aside class="sidebar"> | |
| <div class="logo-area"> | |
| <div class="logo-row"> | |
| <div class="logo-icon">💎</div> | |
| <div class="logo-text"> | |
| <div class="logo-name"><em>Gemma</em> 4</div> | |
| <div class="logo-sub">Google DeepMind</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Model Selector --> | |
| <div class="model-card"> | |
| <div class="field" style="margin-bottom:8px;"> | |
| <span class="field-lbl">Model</span> | |
| <select id="modelSelect" onchange="switchModel(this.value)" style="width:100%;padding:7px 10px;border-radius:8px;border:1.5px solid rgba(109,40,217,.2);background:rgba(109,40,217,.03);font-size:12px;font-weight:600;font-family:var(--fm);color:var(--fg);cursor:pointer;outline:none;"> | |
| <option value="Gemma-4-26B-A4B-it">💎 Gemma 4 26B-A4B (MoE, 3.8B active) — Fast</option> | |
| <option value="Gemma-4-31B-it">💎 Gemma 4 31B (Dense) — Best Quality</option> | |
| </select> | |
| </div> | |
| <div class="mc-top"> | |
| <span class="mc-name" id="mcName">Gemma-4-26B-A4B-it</span> | |
| <span class="mc-arch" id="mcArch">MoE 3.8B/26B</span> | |
| </div> | |
| <div class="mc-stats" id="mcStats"> | |
| <span class="mc-stat mc-hl">GPQA 82.3%</span> | |
| <span class="mc-stat mc-ok">AIME 88.3%</span> | |
| <span class="mc-stat mc-ok">👁️ Vision</span> | |
| <span class="mc-stat mc-ok">256K ctx</span> | |
| </div> | |
| <div class="mc-desc" id="mcDesc">MoE 128 experts · 3.8B active · 31B의 95% 성능, 추론 ~8배 빠름 · 140+ languages</div> | |
| <a href="https://huggingface.co/google/gemma-4-26B-A4B-it" id="mcHfLink" target="_blank" style="display:inline-flex;align-items:center;gap:5px;margin-top:8px;font-size:10px;font-weight:700;color:var(--v);text-decoration:none;padding:4px 10px;border-radius:16px;background:var(--vg);border:1px solid rgba(109,40,217,.15);transition:all .2s;">🤗 Model Card ↗</a> | |
| <div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;"> | |
| <a href="https://huggingface.co/collections/google/gemma-4" target="_blank" style="display:inline-flex;align-items:center;gap:3px;font-size:8px;font-weight:700;color:#059669;text-decoration:none;padding:3px 8px;border-radius:12px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.15);transition:all .2s;">💎 Gemma 4</a> | |
| <a href="https://huggingface.co/spaces/FINAL-Bench/all-bench-leaderboard" target="_blank" style="display:inline-flex;align-items:center;gap:3px;font-size:8px;font-weight:700;color:var(--amber);text-decoration:none;padding:3px 8px;border-radius:12px;background:rgba(217,119,6,.08);border:1px solid rgba(217,119,6,.15);transition:all .2s;">📊 ALL Bench</a> | |
| <a href="https://huggingface.co/spaces/MAYA-AI/all-leaderboard" target="_blank" style="display:inline-flex;align-items:center;gap:3px;font-size:8px;font-weight:700;color:var(--v);text-decoration:none;padding:3px 8px;border-radius:12px;background:rgba(109,40,217,.06);border:1px solid rgba(109,40,217,.12);transition:all .2s;">🌐 MAYA LB</a> | |
| </div> | |
| <div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;"> | |
| <a href="https://huggingface.co/collections/google/gemma-4" target="_blank" style="font-size:8px;font-weight:700;padding:2px 7px;border-radius:10px;background:rgba(22,163,74,.08);border:1px solid rgba(22,163,74,.18);color:#16a34a;text-decoration:none;transition:all .2s;">💎 Gemma 4</a> | |
| <a href="https://huggingface.co/spaces/FINAL-Bench/all-bench-leaderboard" target="_blank" style="font-size:8px;font-weight:700;padding:2px 7px;border-radius:10px;background:rgba(217,119,6,.08);border:1px solid rgba(217,119,6,.18);color:#d97706;text-decoration:none;transition:all .2s;">📊 ALL Bench</a> | |
| <a href="https://huggingface.co/spaces/MAYA-AI/all-leaderboard" target="_blank" style="font-size:8px;font-weight:700;padding:2px 7px;border-radius:10px;background:rgba(109,40,217,.08);border:1px solid rgba(109,40,217,.18);color:var(--v);text-decoration:none;transition:all .2s;">🌐 MAYA LB</a> | |
| </div> | |
| </div> | |
| <!-- Settings --> | |
| <div class="settings"> | |
| <div class="section-lbl">Settings</div> | |
| <div class="toggle-row"> | |
| <div class="toggle-info"><div class="tl">👁️ Vision</div><div class="ts">Image understanding</div></div> | |
| <label class="toggle"><input type="checkbox" id="visionToggle" checked><span class="t-track"></span><span class="t-thumb"></span></label> | |
| </div> | |
| <div class="field"> | |
| <span class="field-lbl">System Prompt</span> | |
| <div class="presets"> | |
| <span class="chip" onclick="setPreset('general')">General</span> | |
| <span class="chip" onclick="setPreset('code')">Code</span> | |
| <span class="chip" onclick="setPreset('math')">Math</span> | |
| <span class="chip" onclick="setPreset('creative')">Creative</span> | |
| <span class="chip" onclick="setPreset('translate')">Translate</span> | |
| <span class="chip" onclick="setPreset('research')">Research</span> | |
| </div> | |
| <textarea id="sysPrompt">You are Gemma 4, a highly capable multimodal AI assistant by Google DeepMind. Think step by step for complex questions.</textarea> | |
| </div> | |
| <div class="sl-wrap"> | |
| <div class="sl-meta"><span class="sl-title">Max Tokens</span><span class="sl-val" id="tokVal">4096</span></div> | |
| <input type="range" id="tokSl" min="64" max="8192" value="4096" step="64" oninput="document.getElementById('tokVal').textContent=this.value"> | |
| </div> | |
| <div class="sl-wrap"> | |
| <div class="sl-meta"><span class="sl-title">Temperature</span><span class="sl-val" id="tempVal">0.60</span></div> | |
| <input type="range" id="tempSl" min="0" max="1.5" value="0.6" step="0.05" oninput="document.getElementById('tempVal').textContent=parseFloat(this.value).toFixed(2)"> | |
| </div> | |
| <div class="sl-wrap"> | |
| <div class="sl-meta"><span class="sl-title">Top-P</span><span class="sl-val" id="topPVal">0.90</span></div> | |
| <input type="range" id="topPSl" min="0.1" max="1.0" value="0.9" step="0.05" oninput="document.getElementById('topPVal').textContent=parseFloat(this.value).toFixed(2)"> | |
| </div> | |
| <button class="clear-btn" onclick="clearChat()">🗑️ Clear conversation</button> | |
| </div> | |
| </aside> | |
| <!-- MAIN --> | |
| <main class="chat-main"> | |
| <header class="chat-hdr"> | |
| <div class="hdr-left"> | |
| <div class="model-pill"><div class="dot"></div><span class="model-name" id="hdrModel">Gemma-4-26B-A4B-it</span></div> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:12px;"> | |
| <a href="https://huggingface.co/google/gemma-4-26B-A4B-it" id="mcHfLink" target="_blank" style="display:inline-flex;align-items:center;gap:6px;padding:5px 14px;border-radius:22px;background:linear-gradient(135deg,rgba(109,40,217,.1),rgba(16,185,129,.08));border:1.5px solid rgba(109,40,217,.25);color:var(--v);font-size:12px;font-weight:700;text-decoration:none;transition:all .22s;font-family:var(--fm);">🤗 Model Card ↗</a> | |
| <a href="https://huggingface.co/collections/google/gemma-4" target="_blank" style="padding:4px 9px;border-radius:14px;background:rgba(22,163,74,.08);border:1px solid rgba(22,163,74,.18);color:#16a34a;font-size:9px;font-weight:700;text-decoration:none;font-family:var(--fm);">💎 Gemma</a> | |
| <a href="https://huggingface.co/spaces/FINAL-Bench/all-bench-leaderboard" target="_blank" style="padding:4px 9px;border-radius:14px;background:rgba(217,119,6,.08);border:1px solid rgba(217,119,6,.18);color:#d97706;font-size:9px;font-weight:700;text-decoration:none;font-family:var(--fm);">📊 ALL</a> | |
| <a href="https://huggingface.co/spaces/MAYA-AI/all-leaderboard" target="_blank" style="padding:4px 9px;border-radius:14px;background:rgba(109,40,217,.08);border:1px solid rgba(109,40,217,.18);color:var(--v);font-size:9px;font-weight:700;text-decoration:none;font-family:var(--fm);">🌐 MAYA</a> | |
| <a class="hf-login-btn" id="loginBtn" href="/oauth/login"> | |
| <svg viewBox="0 0 120 120" width="16" height="16"><path fill="currentColor" d="M41.7 56.6c-5 0-8.8 4-8.8 9s3.8 9 8.8 9 8.8-4 8.8-9-3.8-9-8.8-9zm36.6 0c-5 0-8.8 4-8.8 9s3.8 9 8.8 9 8.8-4 8.8-9-3.8-9-8.8-9z"/></svg> | |
| Sign in with HF | |
| </a> | |
| <div id="userArea" style="display:none;align-items:center;gap:8px;"> | |
| <a class="user-chip" id="userChip" href="#"><div class="user-avatar" id="userAvatar"><span id="userInitial">U</span></div><span id="userName">User</span></a> | |
| <button class="logout-btn" onclick="location.href='/oauth/logout'">Log out</button> | |
| </div> | |
| <div class="hdr-stats"> | |
| <div class="hstat"><div class="hstat-n" id="stMsgs">0</div><div class="hstat-l">Messages</div></div> | |
| <div class="hstat"><div class="hstat-n" id="stTok">0</div><div class="hstat-l">Tokens</div></div> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="img-prev" id="imgPrev"> | |
| <div class="img-tw"> | |
| <img class="img-th" id="imgThumb" src=""> | |
| <div><div class="img-nm" id="imgName">image.jpg</div><div class="img-sz" id="imgSize">—</div></div> | |
| <button class="img-rm" onclick="removeImg()">✕</button> | |
| </div> | |
| </div> | |
| <div class="messages" id="msgs"> | |
| <div class="welcome" id="welcome"> | |
| <div class="welcome-icon">💎</div> | |
| <div class="welcome-title">Hello, I'm <em>Gemma 4</em></div> | |
| <div class="welcome-sub">Google DeepMind's most intelligent open model — Dense 31B or MoE 26B. Upload an image or ask anything. Apache 2.0 licensed.</div> | |
| <div class="welcome-badges"> | |
| <span class="welcome-badge wb-purple">AIME 89.2%</span> | |
| <span class="welcome-badge wb-green">GPQA 84.3%</span> | |
| <span class="welcome-badge wb-amber">256K context</span> | |
| <a href="https://huggingface.co/google/gemma-4-26B-A4B-it" id="mcHfLink" target="_blank" style="font-size:11px;font-weight:600;padding:4px 12px;border-radius:20px;background:rgba(109,40,217,.08);color:var(--v);border:1px solid rgba(109,40,217,.15);text-decoration:none;transition:all .2s;">🤗 Model Card ↗</a> | |
| </div> | |
| <div class="ex-grid"> | |
| <div class="ex-card" onclick="sendEx('Explain how Gemma 4 achieves frontier-level performance with Mixture-of-Experts architecture and hybrid attention.')"><span class="ex-icon">🧬</span><div class="ex-title">Gemma 4 Architecture</div><div class="ex-desc">MoE + hybrid attention</div></div> | |
| <div class="ex-card" onclick="sendEx('Write a Python async web scraper with retry logic and rate limiting. Include type hints.')"><span class="ex-icon">💻</span><div class="ex-title">Code Generation</div><div class="ex-desc">Production-quality code</div></div> | |
| <div class="ex-card" onclick="trySampleVision()"><span class="ex-icon">👁️</span><div class="ex-title">Vision Demo</div><div class="ex-desc">Try image analysis now</div></div> | |
| <div class="ex-card" onclick="sendEx('한국의 K-pop이 세계적으로 성공한 이유를 문화적, 경제적 관점에서 분석해주세요.')"><span class="ex-icon">🌐</span><div class="ex-title">140+ Languages</div><div class="ex-desc">Korean, Japanese, Arabic…</div></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Bar --> | |
| <div class="search-bar" id="searchBar"> | |
| <div class="search-wrap"> | |
| <input class="search-input" id="searchInput" placeholder="Search the web..." onkeydown="if(event.key==='Enter')doSearch()"> | |
| <button class="search-go" onclick="doSearch()">🔍 Search</button> | |
| <button class="icon-btn" onclick="toggleSearch()" style="font-size:12px;">✕</button> | |
| </div> | |
| <div class="search-results" id="searchResults"></div> | |
| </div> | |
| <div class="inputbar"> | |
| <div class="input-wrap"> | |
| <textarea class="chat-ta" id="chatInput" placeholder="Message Gemma 4…" rows="1" onkeydown="handleKey(event)" oninput="autoGrow(this)"></textarea> | |
| <div class="input-acts"> | |
| <label class="icon-btn file-btn" id="fileLabel" title="Upload image or PDF">🖼<input type="file" accept="image/*,.pdf" onchange="handleFile(this)" id="fileInput"></label> | |
| <button class="icon-btn" id="searchToggleBtn" onclick="toggleSearch()" title="Web search">🔍</button> | |
| <button class="icon-btn" onclick="clearChat()" title="New chat" style="font-size:16px;">🧬</button> | |
| <button class="send-btn" id="sendBtn" onclick="sendMsg()">➤</button> | |
| </div> | |
| </div> | |
| <div class="input-hint"> | |
| <span><span class="kbd">Enter</span> send</span> | |
| <span><span class="kbd">Shift+Enter</span> new line</span> | |
| <span class="model-hint" id="modelHint">26B MoE · 3.8B active · 256K ctx</span> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| const S={vision:true,history:[],msgCount:0,totalTok:0,pending:null,busy:false,searchResults:null, | |
| presets:{ | |
| general:'You are Gemma 4, a highly capable multimodal AI assistant by Google DeepMind. Think step by step for complex questions.', | |
| code:'You are an expert software engineer. Write clean, efficient, well-commented code. Explain your approach before writing.', | |
| math:'You are a world-class mathematician. Break problems step-by-step. Show full working.', | |
| creative:'You are a brilliant creative writer. Be imaginative, vivid, and engaging.', | |
| translate:'You are a professional translator fluent in 140+ languages. Provide accurate translations with cultural context.', | |
| research:'You are a rigorous research analyst. Provide structured, well-reasoned analysis.', | |
| } | |
| }; | |
| function apiBase(){return'/gradio';} | |
| // Toggles | |
| document.getElementById('visionToggle').addEventListener('change',function(){ | |
| S.vision=this.checked; | |
| const l=document.getElementById('fileLabel'); | |
| l.style.opacity=this.checked?'1':'.35'; | |
| l.style.pointerEvents=this.checked?'auto':'none'; | |
| if(!this.checked)removeImg(); | |
| }); | |
| function setPreset(k){document.getElementById('sysPrompt').value=S.presets[k];showToast('Preset applied ✓');} | |
| // File handling (image + PDF) | |
| function handleFile(input){ | |
| const f=input.files[0];if(!f)return; | |
| if(f.type==='application/pdf'){ | |
| // PDF handling | |
| const r=new FileReader(); | |
| r.onload=async e=>{ | |
| S.pending={type:'pdf',data:e.target.result,name:f.name,size:fmtSz(f.size)}; | |
| document.getElementById('imgThumb').src=''; | |
| document.getElementById('imgThumb').style.display='none'; | |
| document.getElementById('imgName').textContent='📄 '+f.name; | |
| document.getElementById('imgSize').textContent=fmtSz(f.size); | |
| document.getElementById('imgPrev').classList.add('show'); | |
| // Extract text | |
| try{ | |
| const res=await fetch('/api/extract-pdf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({data:e.target.result})}); | |
| const data=await res.json(); | |
| if(data.text){S.pending.text=data.text;document.getElementById('imgSize').textContent=`${fmtSz(f.size)} · ${data.chars} chars extracted`;} | |
| }catch(err){console.error('PDF extract failed:',err);} | |
| }; | |
| r.readAsDataURL(f); | |
| }else{ | |
| // Image handling | |
| const r=new FileReader(); | |
| r.onload=e=>{ | |
| S.pending={type:'image',data:e.target.result,name:f.name,size:fmtSz(f.size)}; | |
| document.getElementById('imgThumb').src=e.target.result; | |
| document.getElementById('imgThumb').style.display='block'; | |
| document.getElementById('imgName').textContent=f.name; | |
| document.getElementById('imgSize').textContent=fmtSz(f.size); | |
| document.getElementById('imgPrev').classList.add('show'); | |
| }; | |
| r.readAsDataURL(f); | |
| } | |
| } | |
| function removeImg(){S.pending=null;document.getElementById('imgPrev').classList.remove('show');document.getElementById('fileInput').value='';document.getElementById('imgThumb').style.display='block';} | |
| function fmtSz(b){return b<1048576?`${(b/1024).toFixed(0)} KB`:`${(b/1048576).toFixed(1)} MB`;} | |
| // Vision sample | |
| function trySampleVision(){ | |
| const sampleUrl='https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/rabbit.png'; | |
| S.pending={type:'image',data:sampleUrl,name:'sample_rabbit.png',size:'sample',isUrl:true}; | |
| document.getElementById('chatInput').value='Describe this image in detail. What story does it tell?'; | |
| sendMsg(); | |
| } | |
| // Search | |
| function toggleSearch(){ | |
| const bar=document.getElementById('searchBar'); | |
| bar.classList.toggle('show'); | |
| if(bar.classList.contains('show'))document.getElementById('searchInput').focus(); | |
| } | |
| async function doSearch(){ | |
| const q=document.getElementById('searchInput').value.trim(); | |
| if(!q)return; | |
| const res=document.getElementById('searchResults'); | |
| res.innerHTML='<div style="color:var(--ink4);font-size:10px;">🔍 Searching...</div>'; | |
| res.classList.add('show'); | |
| try{ | |
| const r=await fetch('/api/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q})}); | |
| const data=await r.json(); | |
| if(data.error){res.innerHTML=`<div style="color:var(--rose);font-size:10px;">❌ ${data.error}</div>`;return;} | |
| if(!data.results||data.results.length===0){res.innerHTML='<div style="color:var(--ink4);">No results found</div>';return;} | |
| let html=data.results.map(r=>`<div class="sr-item"><div class="sr-title">${esc(r.title)}</div><div class="sr-desc">${esc(r.desc)}</div><a class="sr-url" href="${r.url}" target="_blank">${r.url}</a></div>`).join(''); | |
| html+=`<button style="margin-top:6px;padding:4px 10px;border-radius:6px;border:1px solid var(--line);background:var(--vg);color:var(--v);font-size:10px;font-weight:600;cursor:pointer;font-family:var(--fb);" onclick="injectSearch()">📎 Inject into chat</button>`; | |
| res.innerHTML=html; | |
| S.searchResults=data.results; | |
| }catch(e){res.innerHTML=`<div style="color:var(--rose);">Error: ${e.message}</div>`;} | |
| } | |
| function injectSearch(){ | |
| if(!S.searchResults)return; | |
| const ctx=S.searchResults.map(r=>`• ${r.title}: ${r.desc}`).join('\n'); | |
| const inp=document.getElementById('chatInput'); | |
| const q=document.getElementById('searchInput').value.trim(); | |
| inp.value=`Based on the following web search results for "${q}":\n\n${ctx}\n\nPlease answer: ${q}`; | |
| document.getElementById('searchBar').classList.remove('show'); | |
| document.getElementById('searchResults').classList.remove('show'); | |
| autoGrow(inp); | |
| showToast('Search results injected ✓'); | |
| } | |
| // Send | |
| function handleKey(e){if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();sendMsg();}} | |
| function autoGrow(el){el.style.height='auto';el.style.height=Math.min(el.scrollHeight,150)+'px';} | |
| async function sendMsg(){ | |
| if(S.busy)return; | |
| const inp=document.getElementById('chatInput'); | |
| const msg=inp.value.trim();if(!msg)return; | |
| inp.value='';inp.style.height='auto'; | |
| document.getElementById('sendBtn').disabled=true; | |
| S.busy=true; | |
| hideWelcome(); | |
| const imgSnap=S.pending?{...S.pending}:null; | |
| removeImg(); | |
| // PDF: prepend extracted text to message | |
| let finalMsg=msg; | |
| let imgData=null; | |
| if(imgSnap&&imgSnap.type==='pdf'&&imgSnap.text){ | |
| finalMsg=`[Document: ${imgSnap.name}]\n\n${imgSnap.text}\n\n---\n\n${msg}`; | |
| appendUser(msg,null,'📄 '+imgSnap.name); | |
| }else if(imgSnap&&imgSnap.type==='image'){ | |
| imgData=imgSnap.isUrl?null:imgSnap.data; // URL images handled differently | |
| appendUser(msg,imgSnap); | |
| }else{ | |
| appendUser(msg); | |
| } | |
| const startTime=performance.now(); | |
| const payload=[ | |
| finalMsg,S.history,'⚡ Fast Mode (direct answer)', | |
| imgData, | |
| document.getElementById('sysPrompt').value.trim()||S.presets.general, | |
| parseInt(document.getElementById('tokSl').value), | |
| parseFloat(document.getElementById('tempSl').value), | |
| parseFloat(document.getElementById('topPSl').value), | |
| document.getElementById('modelSelect').value, | |
| ]; | |
| const botDiv=appendBot(); | |
| try{ | |
| const base=apiBase(); | |
| const postRes=await fetch(`${base}/gradio_api/call/chat`,{ | |
| method:'POST',headers:{'Content-Type':'application/json'}, | |
| body:JSON.stringify({data:payload}), | |
| }); | |
| if(!postRes.ok)throw new Error(`HTTP ${postRes.status}: ${await postRes.text()}`); | |
| const{event_id}=await postRes.json(); | |
| if(!event_id)throw new Error('No event_id from Gradio'); | |
| await new Promise((resolve,reject)=>{ | |
| const es=new EventSource(`${base}/gradio_api/call/chat/${event_id}`); | |
| let raw='',done=false; | |
| const finish=text=>{ | |
| if(done)return;done=true;es.close(); | |
| const elapsed=(performance.now()-startTime)/1000; | |
| const tokens=Math.round(text.length/4); | |
| const speed=tokens/elapsed; | |
| renderBot(botDiv,text,elapsed,tokens,speed); | |
| S.history.push([msg,stripThink(text)]); | |
| S.msgCount+=2;S.totalTok+=tokens; | |
| updateStats(); | |
| resolve(); | |
| }; | |
| es.addEventListener('generating',e=>{ | |
| try{const d=JSON.parse(e.data);if(Array.isArray(d)&&d[0]!=null){raw=String(d[0]);renderBot(botDiv,raw);}}catch{} | |
| }); | |
| es.addEventListener('complete',e=>{try{const d=JSON.parse(e.data);finish(Array.isArray(d)?String(d[0]??raw):raw);}catch{finish(raw);}}); | |
| es.addEventListener('process_completed',e=>{try{const d=JSON.parse(e.data);finish(String(d?.output?.data?.[0]??raw));}catch{finish(raw);}}); | |
| es.onmessage=e=>{ | |
| if(done)return; | |
| try{const d=JSON.parse(e.data);if(d&&d.msg==='process_completed')finish(String(d?.output?.data?.[0]??raw));else if(Array.isArray(d)&&d[0]!=null){raw=String(d[0]);renderBot(botDiv,raw);}}catch{} | |
| }; | |
| es.onerror=()=>{if(done)return;if(raw&&raw.length>0){finish(raw);}else{done=true;es.close();reject(new Error('SSE stream ended'));}}; | |
| setTimeout(()=>{if(!done){done=true;es.close();reject(new Error('Request timed out'));}},300000); | |
| }); | |
| }catch(err){ | |
| renderBot(botDiv,`**Error:** ${esc(err.message)}\n\n_Check if SGLang server is running._`); | |
| } | |
| S.busy=false; | |
| document.getElementById('sendBtn').disabled=false; | |
| scrollDown(); | |
| } | |
| function sendEx(t){document.getElementById('chatInput').value=t;sendMsg();} | |
| function hideWelcome(){const w=document.getElementById('welcome');if(w){w.style.transition='opacity .25s';w.style.opacity='0';setTimeout(()=>w.remove(),260);}} | |
| function appendUser(text,img,pdfLabel){ | |
| const t=new Date().toLocaleTimeString('en',{hour:'2-digit',minute:'2-digit'}); | |
| let extra=''; | |
| if(img&&img.type==='image')extra=`<img src="${img.data}" style="max-width:180px;border-radius:9px;margin-bottom:6px;display:block;">`; | |
| if(pdfLabel)extra=`<span class="pdf-badge">${esc(pdfLabel)}</span><br>`; | |
| const el=document.createElement('div'); | |
| el.className='msg user'; | |
| el.innerHTML=`<div class="avatar">U</div><div class="body">${extra}<div class="bubble">${esc(text)}</div><div class="msg-time">${t}</div></div>`; | |
| document.getElementById('msgs').appendChild(el); | |
| scrollDown(); | |
| } | |
| function appendBot(){ | |
| const el=document.createElement('div'); | |
| el.className='msg bot'; | |
| el.innerHTML='<div class="avatar">🧬</div><div class="body"><div class="bubble"><div class="typing"><span></span><span></span><span></span></div></div><div class="msg-time">—</div><div class="msg-badges"></div></div>'; | |
| document.getElementById('msgs').appendChild(el); | |
| scrollDown();return el; | |
| } | |
| function isThinkLine(l){ | |
| l=l.trim(); | |
| if(!l)return true; | |
| const starts=['The user','the user','This is','this is','I should','I need to','Let me','let me', | |
| 'My task','my task',"I'll ",'I will','Since ','since ','Now,','now,','So,','so,','First,','first,', | |
| 'Okay','okay','Alright','Hmm','Wait','Actually','The question','the question','The input', | |
| 'The request','The prompt','Thinking Process','**Thinking','Step ','step ','Approach:','Analysis:', | |
| 'Reasoning:','1. **','2. **','3. **','4. **','5. **']; | |
| for(const s of starts)if(l.startsWith(s))return true; | |
| if((l.startsWith('- ')||l.startsWith('* ')||l.startsWith('○ '))&&!/[\u1100-\uD7AF\uAC00-\uD7A3]/.test(l.slice(0,30)))return true; | |
| return false; | |
| } | |
| function splitThinkAnswer(raw){ | |
| const lines=raw.split('\n'); | |
| for(let i=0;i<lines.length;i++){ | |
| const l=lines[i].trim(); | |
| if(!isThinkLine(lines[i])){ | |
| if(/[\uAC00-\uD7A3]/.test(l.slice(0,10))){return[lines.slice(0,i).join('\n').trim(),lines.slice(i).join('\n').trim()];} | |
| if(i>2&&lines.slice(Math.max(0,i-2),i).every(x=>!x.trim())){return[lines.slice(0,i).join('\n').trim(),lines.slice(i).join('\n').trim()];} | |
| } | |
| } | |
| return['',raw]; | |
| } | |
| function renderBot(el,raw,elapsed,tokens,speed){ | |
| const t=new Date().toLocaleTimeString('en',{hour:'2-digit',minute:'2-digit'}); | |
| el.querySelector('.msg-time').textContent=t; | |
| const b=el.querySelector('.bubble'); | |
| const badges=el.querySelector('.msg-badges'); | |
| let thinkHtml='',ans=raw,thinkChars=0; | |
| const tOpen=raw.indexOf('<think>'),tClose=raw.indexOf('</think>'); | |
| const dOpen=raw.indexOf('<details>'),sClose=raw.indexOf('</summary>'),dClose=raw.indexOf('</details>'); | |
| if(tOpen>=0&&tClose>tOpen){ | |
| const chain=raw.slice(tOpen+7,tClose).trim();ans=raw.slice(tClose+8).trim(); | |
| thinkHtml=buildThink(chain);thinkChars=chain.length; | |
| }else if(tOpen>=0){ | |
| const thinkLen=raw.length-tOpen-7;ans=''; | |
| thinkHtml=`<div class="think-blk"><div class="think-hdr" style="cursor:default;">🧠 REASONING (${thinkLen} CHARS) <span class="typing" style="display:inline-flex;gap:4px;vertical-align:middle;margin-left:6px;"><span></span><span></span><span></span></span></div></div>`; | |
| }else if(dOpen>=0&&sClose>dOpen){ | |
| const body=dClose>sClose?raw.slice(sClose+10,dClose):raw.slice(sClose+10); | |
| const chain=body.replace(/^> ?/gm,'').replace(/<[^>]+>/g,'').trim(); | |
| ans=dClose>0?raw.slice(dClose+10).trim():'';thinkHtml=buildThink(chain);thinkChars=chain.length; | |
| }else{ | |
| const firstLine=raw.trim().split('\n')[0]||''; | |
| if(isThinkLine(firstLine)&&raw.length>20){ | |
| const[think,answer]=splitThinkAnswer(raw); | |
| if(think&&answer){thinkHtml=buildThink(think);ans=answer;thinkChars=think.length;} | |
| else if(think&&!answer){ | |
| ans=''; | |
| thinkHtml=`<div class="think-blk"><div class="think-hdr" style="cursor:default;">🧠 REASONING (${raw.length} CHARS) <span class="typing" style="display:inline-flex;gap:4px;vertical-align:middle;margin-left:6px;"><span></span><span></span><span></span></span></div></div>`; | |
| } | |
| } | |
| } | |
| b.innerHTML=thinkHtml+md(ans); | |
| // Speed + reasoning badges (only on final render) | |
| if(elapsed&&badges){ | |
| let bh=`<span class="speed-badge">⚡ ${elapsed.toFixed(1)}s · ${tokens} tok · ${speed.toFixed(0)} tok/s</span>`; | |
| if(thinkChars>0)bh+=`<span class="reason-badge">🧠 Deep Reasoning · ${thinkChars.toLocaleString()} chars analyzed</span>`; | |
| badges.innerHTML=bh; | |
| } | |
| } | |
| function buildThink(chain){ | |
| if(!chain)return''; | |
| return`<div class="think-blk" onclick="this.classList.toggle('open')"><div class="think-hdr">🧠 Reasoning Chain (${chain.length} chars)</div><div class="think-body">${esc(chain)}</div></div>`; | |
| } | |
| function stripThink(t){ | |
| t=t.replace(/^<details>[\s\S]*?<\/details>\n\n/,'').replace(/^<think>[\s\S]*?<\/think>\s*/,''); | |
| // Strip plain-text thinking | |
| t=t.replace(/^🧠 Reasoning.*?\n?/,''); | |
| return t; | |
| } | |
| // Marked.js + code copy | |
| (function setupMarked(){ | |
| if(typeof marked==='undefined')return; | |
| const renderer=new marked.Renderer(); | |
| renderer.code=(code,lang)=>{ | |
| const language=lang&&hljs.getLanguage(lang)?lang:'plaintext'; | |
| const codeText=typeof code==='object'?code.text:code; | |
| const highlighted=hljs.highlight(codeText,{language}).value; | |
| const langLabel=language!=='plaintext'?language:'code'; | |
| const escaped=codeText.replace(/'/g,"\\'").replace(/\n/g,"\\n"); | |
| return`<div class="code-wrap"><div class="code-header"><span class="code-lang-tag">${langLabel}</span><button class="copy-btn" onclick="copyCode(this,'${escaped}')">📋 Copy</button></div><pre style="border-radius:0 0 8px 8px;margin:0;border-top:none;"><code class="hljs language-${language}">${highlighted}</code></pre></div>`; | |
| }; | |
| renderer.codespan=code=>`<code style="background:var(--fog);padding:1px 5px;border-radius:4px;font-family:var(--fm);font-size:11.5px">${code}</code>`; | |
| renderer.link=(href,title,text)=>`<a href="${typeof href==='object'?href.href:href}" target="_blank" rel="noopener">${text}</a>`; | |
| marked.setOptions({renderer,breaks:true,gfm:true}); | |
| })(); | |
| function copyCode(btn,code){ | |
| const text=code.replace(/\\n/g,'\n').replace(/\\'/g,"'"); | |
| navigator.clipboard.writeText(text).then(()=>{ | |
| btn.textContent='✅ Copied!';btn.classList.add('copied'); | |
| setTimeout(()=>{btn.textContent='📋 Copy';btn.classList.remove('copied');},2000); | |
| }); | |
| } | |
| function md(t){if(!t)return'';if(typeof marked!=='undefined'){try{return marked.parse(t);}catch(e){}}return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/`([^`]+)`/g,'<code>$1</code>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\n/g,'<br>');} | |
| function esc(t){return t?t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'):'';} | |
| function scrollDown(){const m=document.getElementById('msgs');m.scrollTop=m.scrollHeight;} | |
| function updateStats(){document.getElementById('stMsgs').textContent=S.msgCount;const t=S.totalTok;document.getElementById('stTok').textContent=t>999?(t/1000).toFixed(1)+'k':t;} | |
| function clearChat(){ | |
| S.history=[];S.msgCount=0;S.totalTok=0;updateStats(); | |
| document.getElementById('msgs').innerHTML='<div class="welcome" id="welcome" style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;min-height:400px;text-align:center;padding:30px 36px;gap:20px;"><div class="welcome-icon" style="width:72px;height:72px;border-radius:20px;background:linear-gradient(135deg,#6d28d9,#a78bfa,#10b981);display:flex;align-items:center;justify-content:center;font-size:34px;box-shadow:0 8px 30px rgba(109,40,217,.25)">💎</div><div class="welcome-title" style="font-family:var(--fd);font-size:34px">Hello, I\'m <em>Gemma 4</em></div></div>'; | |
| } | |
| function showToast(msg,dur=2200){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),dur);} | |
| // HF OAuth | |
| async function checkAuth(){try{const res=await fetch('/oauth/user');if(res.ok){const u=await res.json();if(u.logged_in){showLoggedIn(u);return;}}}catch(e){}showLoggedOut();} | |
| document.addEventListener('visibilitychange',()=>{if(!document.hidden)checkAuth();}); | |
| function showLoggedIn(u){ | |
| document.getElementById('loginBtn').style.display='none'; | |
| const ua=document.getElementById('userArea');ua.style.display='flex'; | |
| document.getElementById('userName').textContent=u.username||u.name||'User'; | |
| document.getElementById('userChip').href=u.profile||'https://huggingface.co'; | |
| const av=document.getElementById('userAvatar'); | |
| if(u.avatar){av.innerHTML='<img src="'+u.avatar+'" alt="av">';} | |
| else{document.getElementById('userInitial').textContent=(u.username||'U')[0].toUpperCase();} | |
| } | |
| function showLoggedOut(){document.getElementById('loginBtn').style.display='inline-flex';document.getElementById('userArea').style.display='none';} | |
| checkAuth(); | |
| // ── Model Switching ── | |
| const MODEL_INFO={ | |
| 'Gemma-4-26B-A4B-it':{arch:'MoE 3.8B/26B',stats:'<span class="mc-stat mc-hl">GPQA 82.3%</span><span class="mc-stat mc-ok">AIME 88.3%</span><span class="mc-stat mc-ok">👁️ Vision</span><span class="mc-stat mc-ok">256K ctx</span>',desc:'MoE 128 experts · 3.8B active · 31B의 95% 성능, 추론 ~8배 빠름 · 140+ languages',hint:'26B MoE · 3.8B active · 256K ctx',hf:'https://huggingface.co/google/gemma-4-26B-A4B-it'}, | |
| 'Gemma-4-31B-it':{arch:'Dense 31B',stats:'<span class="mc-stat mc-hl">AIME 89.2%</span><span class="mc-stat mc-ok">GPQA 84.3%</span><span class="mc-stat mc-ok">👁️ Vision</span><span class="mc-stat mc-ok">256K ctx</span>',desc:'Dense 31B · 최고 품질 · Codeforces 2150 · Arena 오픈 모델 3위 · 140+ languages',hint:'31B Dense · 30.7B active · 256K ctx',hf:'https://huggingface.co/google/gemma-4-31B-it'}, | |
| }; | |
| function switchModel(name){ | |
| const m=MODEL_INFO[name];if(!m)return; | |
| document.getElementById('mcName').textContent=name; | |
| document.getElementById('mcArch').textContent=m.arch; | |
| document.getElementById('mcStats').innerHTML=m.stats; | |
| document.getElementById('mcDesc').textContent=m.desc; | |
| document.getElementById('hdrModel').textContent=name; | |
| document.getElementById('modelHint').textContent=m.hint; | |
| try{document.getElementById('mcHfLink').href=m.hf;}catch(e){} | |
| try{document.getElementById('hdrHfLink').href=m.hf;}catch(e){} | |
| showToast('Model: '+name+' ✓'); | |
| } | |
| </script> | |
| </body> | |
| </html> |