Download index.html from Bc-AI/nova-chat-ui: direct link, hf CLI and curl.
- Browser
- Download file 35 kB
-
https://huggingface.co/Bc-AI/nova-chat-ui/resolve/main/index.html
- Command line
-
hf download hf://Bc-AI/nova-chat-ui/index.html
-
curl -L -o index.html https://huggingface.co/Bc-AI/nova-chat-ui/resolve/main/index.html
35 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nova-1 Chat</title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); | |
| :root { | |
| --surface: #212121; | |
| --surface2: #171717; | |
| --surface3: #2f2f2f; | |
| --sidebar-bg: #171717; | |
| --text: #ececec; | |
| --text2: #b4b4b4; | |
| --text3: #7e7e7e; | |
| --accent: #ffffff; | |
| --accent-button: #676767; | |
| --border: #3a3a3a; | |
| --hover: #2f2f2f; | |
| --user-bubble: #2f2f2f; | |
| --shadow-sm: 0 1px 2px rgba(0,0,0,.3); | |
| --radius-lg: 18px; | |
| --radius-md: 10px; | |
| --radius-sm: 6px; | |
| --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif; | |
| --font-mono: 'JetBrains Mono', 'SF Mono', monospace; | |
| } | |
| *{box-sizing:border-box;margin:0;padding:0} | |
| html,body{height:100vh;overflow:hidden;background:var(--surface);font-family:var(--font-sans);color:var(--text);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased} | |
| body{display:flex} | |
| /* ── SIDEBARS ── */ | |
| .sidebar{background:var(--sidebar-bg);height:100vh;display:flex;flex-direction:column;overflow:hidden;transition:width .35s cubic-bezier(.22,1,.36,1),min-width .35s cubic-bezier(.22,1,.36,1),opacity .35s ease;position:relative;z-index:10} | |
| #left-sidebar{width:260px;min-width:260px;border-right:1px solid var(--border)} | |
| #left-sidebar.collapsed{width:0;min-width:0;opacity:0;pointer-events:none;border-right:none} | |
| #right-sidebar{width:280px;min-width:280px;border-left:1px solid var(--border)} | |
| #right-sidebar.collapsed{width:0;min-width:0;opacity:0;pointer-events:none;border-left:none} | |
| .ls-header{padding:18px 16px 14px;border-bottom:1px solid var(--border)} | |
| .ls-header h1{font-size:1.1rem;font-weight:700;letter-spacing:-.01em;color:var(--text)} | |
| .ls-header .ls-sub{font-size:.68rem;color:var(--text3);margin-top:2px} | |
| .new-chat-btn{margin:12px 14px;background:transparent;border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 14px;font-weight:500;font-size:.8rem;color:var(--text);cursor:pointer;width:calc(100% - 28px);transition:background .15s;font-family:inherit;display:flex;align-items:center;gap:8px} | |
| .new-chat-btn svg{width:16px;height:16px;opacity:.7} | |
| .new-chat-btn:hover{background:var(--hover)} | |
| .ls-chats-label{padding:20px 16px 8px;font-size:.65rem;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.06em} | |
| .ls-chat-item{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);font-size:.82rem;margin:0 8px 2px;transition:background .12s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
| .ls-chat-item:hover{background:var(--hover);color:var(--text)} | |
| .ls-chat-item.active{background:var(--hover);color:var(--text)} | |
| .ls-footer{margin-top:auto;padding:14px 16px;border-top:1px solid var(--border);font-size:.68rem;color:var(--text3);text-align:center} | |
| .rs-header{padding:18px 16px 12px;border-bottom:1px solid var(--border)} | |
| .rs-header h2{font-size:.75rem;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em} | |
| .right-body{flex:1;overflow-y:auto;padding:14px 16px} | |
| .right-body::-webkit-scrollbar{width:4px} | |
| .right-body::-webkit-scrollbar-thumb{background:#444;border-radius:3px} | |
| .rs-section{margin-bottom:22px} | |
| .rs-section-title{font-size:.65rem;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px} | |
| .param-group{margin-bottom:10px} | |
| .param-group label{display:block;font-size:.75rem;color:var(--text2);margin-bottom:4px} | |
| .param-group input[type="range"]{width:100%;accent-color:var(--accent);cursor:pointer;height:4px} | |
| .param-val{float:right;color:var(--text2);font-weight:500;font-size:.72rem} | |
| #system-prompt{background:var(--surface3);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);padding:8px 10px;font-size:.76rem;width:100%;font-family:inherit;resize:vertical;min-height:60px} | |
| #system-prompt:focus{outline:none;border-color:#888} | |
| .mode-group{display:flex;gap:6px} | |
| .mode-btn{flex:1;background:var(--surface3);border:1px solid var(--border);color:var(--text2);border-radius:var(--radius-sm);padding:7px 0;font-size:.72rem;cursor:pointer;text-align:center;transition:all .12s;font-family:inherit} | |
| .mode-btn.active{background:var(--hover);border-color:#777;color:var(--text);font-weight:500} | |
| #yarn-btn{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:var(--radius-md);padding:10px 14px;width:100%;cursor:pointer;font-size:.78rem;font-weight:500;transition:all .15s;font-family:inherit;display:flex;align-items:center;gap:8px} | |
| #yarn-btn svg{width:18px;height:18px;opacity:.7} | |
| #yarn-btn:hover{background:var(--hover);border-color:#666} | |
| .yarn-current{display:inline-block;background:var(--hover);color:var(--text2);padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:500;margin-top:8px} | |
| /* ── MAIN CHAT ── */ | |
| #chat-main{flex:1;display:flex;flex-direction:column;height:100vh;background:var(--surface);min-width:0;position:relative} | |
| .toggle-btn{position:absolute;top:18px;z-index:20;width:32px;height:32px;border-radius:50%;background:transparent;border:1px solid var(--border);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;font-size:1rem} | |
| .toggle-btn:hover{background:var(--hover);color:var(--text)} | |
| #toggle-left{left:12px} | |
| #toggle-right{right:12px} | |
| /* ── MESSAGES AREA ── */ | |
| #chat-messages{flex:1;overflow-y:auto;overflow-x:hidden} | |
| #chat-messages::-webkit-scrollbar{width:5px} | |
| #chat-messages::-webkit-scrollbar-thumb{background:#444;border-radius:4px} | |
| #messages-inner{max-width:720px;width:100%;margin:0 auto;padding:20px 20px 0;display:flex;flex-direction:column;min-height:100%} | |
| /* when empty, center welcome */ | |
| #messages-inner.empty{justify-content:center;align-items:center;padding-bottom:20px} | |
| .welcome-block{text-align:center;margin-bottom:0;animation:fadeUp .6s cubic-bezier(.22,1,.36,1)} | |
| .welcome-block h2{font-size:1.5rem;font-weight:600;letter-spacing:-.01em;margin-bottom:6px} | |
| .welcome-block p{font-size:.85rem;color:var(--text3);max-width:400px;line-height:1.6} | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} | |
| /* ── MESSAGE ROWS ── */ | |
| .msg-row{display:flex;margin-bottom:28px;animation:msgIn .4s cubic-bezier(.22,1,.36,1);position:relative} | |
| .msg-row.user{justify-content:flex-end} | |
| .msg-row.bot{justify-content:flex-start} | |
| @keyframes msgIn{from{opacity:0;transform:translateY(16px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}} | |
| .bubble-user{background:var(--user-bubble);color:var(--text);border-radius:var(--radius-lg) var(--radius-lg) 4px var(--radius-lg);padding:11px 17px;max-width:75%;font-size:.92rem;line-height:1.65;white-space:pre-wrap;word-break:break-word} | |
| .bubble-bot{color:var(--text);max-width:100%;font-size:.92rem;line-height:1.65;white-space:pre-wrap;word-break:break-word;padding:0} | |
| .bubble-bot pre{background:#1a1a1a;border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;overflow-x:auto;font-size:.78rem;margin:10px 0;font-family:var(--font-mono)} | |
| .bubble-bot code{background:#1a1a1a;padding:2px 5px;border-radius:4px;font-size:.85em;font-family:var(--font-mono)} | |
| .bubble-bot pre code{padding:0;background:transparent} | |
| /* ── ACTIONS UNDER BOT MESSAGES ── */ | |
| .msg-actions{display:flex;gap:2px;margin-top:6px;opacity:0;transform:translateY(-4px);transition:opacity .2s,transform .2s;pointer-events:none} | |
| .msg-row.bot:hover .msg-actions, | |
| .msg-row.bot:last-child .msg-actions{opacity:1;transform:translateY(0);pointer-events:auto} | |
| .msg-actions button{background:transparent;border:none;color:var(--text3);border-radius:6px;padding:4px 8px;font-size:.72rem;cursor:pointer;transition:all .12s;font-family:inherit;display:flex;align-items:center;gap:4px} | |
| .msg-actions button:hover{color:var(--text);background:var(--hover)} | |
| .msg-actions button svg{width:13px;height:13px;opacity:.7} | |
| /* typing */ | |
| .typing-indicator{display:inline-flex;gap:5px;padding:4px 0} | |
| .typing-indicator span{width:6px;height:6px;border-radius:50%;background:var(--text3);animation:bounce 1.2s infinite} | |
| .typing-indicator span:nth-child(2){animation-delay:.15s} | |
| .typing-indicator span:nth-child(3){animation-delay:.3s} | |
| @keyframes bounce{0%,60%,100%{transform:translateY(0);opacity:.3}30%{transform:translateY(-7px);opacity:1}} | |
| /* ── INPUT AREA (always at bottom) ── */ | |
| #input-area{flex-shrink:0;padding:0 20px 16px} | |
| #input-inner{max-width:720px;width:100%;margin:0 auto} | |
| #input-row{display:flex;align-items:flex-end;background:var(--surface3);border:1px solid var(--border);border-radius:28px;padding:5px 5px 5px 18px;transition:border-color .2s,box-shadow .2s} | |
| #input-row:focus-within{border-color:#777;box-shadow:0 0 0 1px rgba(255,255,255,.05)} | |
| #msg-input{flex:1;background:transparent;border:none;color:var(--text);padding:9px 0;font-size:.88rem;resize:none;outline:none;font-family:inherit;line-height:1.5;max-height:160px} | |
| #msg-input::placeholder{color:var(--text3)} | |
| #send-btn{width:36px;height:36px;min-width:36px;border-radius:50%;background:var(--text);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0;color:var(--surface)} | |
| #send-btn svg{width:16px;height:16px} | |
| #send-btn:hover{background:#ddd} | |
| #send-btn:disabled{opacity:.3;cursor:default;background:var(--text3)} | |
| .input-hint{text-align:center;font-size:.68rem;color:var(--text3);margin-top:8px} | |
| /* ── YaRN MODAL ── */ | |
| #yarn-modal{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.65);backdrop-filter:blur(6px)} | |
| #yarn-modal.show{display:flex;animation:modalFade .2s ease} | |
| @keyframes modalFade{from{opacity:0}to{opacity:1}} | |
| .modal-card{background:var(--surface2);border:1px solid var(--border);border-radius:18px;width:440px;max-width:92vw;padding:28px;box-shadow:0 24px 64px rgba(0,0,0,.6);animation:cardPop .35s cubic-bezier(.22,1,.36,1)} | |
| @keyframes cardPop{from{opacity:0;transform:translateY(24px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}} | |
| .modal-card h3{font-size:1.05rem;font-weight:600;margin-bottom:4px;letter-spacing:-.01em} | |
| .modal-desc{font-size:.78rem;color:var(--text2);margin-bottom:16px;line-height:1.55} | |
| .modal-warning{background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);color:#d4a81a;padding:10px 14px;border-radius:var(--radius-sm);font-size:.72rem;margin-bottom:18px;line-height:1.55} | |
| .yarn-options{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:18px} | |
| .yarn-option{background:var(--surface3);border:1.5px solid var(--border);border-radius:12px;padding:14px;text-align:center;cursor:pointer;transition:all .18s;position:relative} | |
| .yarn-option:hover{border-color:#555;transform:translateY(-1px)} | |
| .yarn-option.selected{border-color:#999;background:rgba(255,255,255,.04);box-shadow:0 0 12px rgba(255,255,255,.04)} | |
| .yarn-option .y-label{font-size:.92rem;font-weight:700;color:var(--text)} | |
| .yarn-option.selected .y-label{color:var(--text)} | |
| .yarn-option .y-factor{font-size:.68rem;color:var(--text2);margin-top:3px} | |
| .yarn-option .y-native{font-size:.6rem;color:#22c55e;margin-top:2px;font-weight:600} | |
| /* YaRN animation */ | |
| #yarn-stretch-anim{display:none;margin:0 0 16px;text-align:center} | |
| #yarn-stretch-anim.show{display:block;padding:12px 4px 0} | |
| .stretch-visual{display:flex;align-items:center;justify-content:center;margin:0 auto 20px;position:relative;height:80px;width:100%} | |
| .helix-svg{width:100%;height:80px;overflow:visible} | |
| .helix-path{stroke:var(--text3);stroke-width:2.5;fill:none;stroke-dasharray:0 400;transition:stroke-dasharray .15s ease;stroke-linecap:round} | |
| .helix-path.active{stroke:#c4c4c4} | |
| .helix-path.done{stroke:#22c55e} | |
| .stretch-info{font-size:.78rem;color:var(--text2);margin-bottom:14px;text-align:center} | |
| .stretch-bar-wrap{background:var(--surface3);border-radius:12px;height:8px;width:100%;overflow:hidden;margin-bottom:14px} | |
| .stretch-bar{height:100%;background:var(--text3);border-radius:12px;width:0%;transition:width .15s ease} | |
| .stretch-bar.active{background:var(--text)} | |
| .stretch-bar.done{background:#22c55e} | |
| .stretch-bar-label{display:flex;justify-content:space-between;font-size:.68rem;color:var(--text3);margin-bottom:6px;padding:0 2px} | |
| .stretch-check{font-size:.82rem;color:#22c55e;margin-top:12px;display:none;font-weight:500;text-align:center} | |
| .stretch-check.show{display:block;animation:checkPop .35s cubic-bezier(.22,1,.36,1)} | |
| @keyframes checkPop{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}} | |
| .modal-buttons{display:flex;gap:10px;justify-content:flex-end} | |
| .modal-buttons button{border:none;border-radius:var(--radius-sm);padding:9px 20px;font-size:.8rem;font-weight:500;cursor:pointer;transition:all .15s;font-family:inherit} | |
| .modal-cancel{background:#333;color:var(--text2)} | |
| .modal-cancel:hover{background:#444;color:var(--text)} | |
| .modal-apply{background:var(--text);color:#111;font-weight:600} | |
| .modal-apply:hover{background:#ddd;transform:translateY(-1px)} | |
| .modal-apply:disabled{opacity:.4;cursor:not-allowed;transform:none} | |
| @media (max-width:768px){ | |
| #left-sidebar,#right-sidebar{display:none} | |
| #toggle-left,#toggle-right{display:none} | |
| #chat-messages{padding:0} | |
| #messages-inner{padding:16px 12px 0} | |
| #input-area{padding:0 12px 12px} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- LEFT SIDEBAR --> | |
| <div class="sidebar" id="left-sidebar"> | |
| <div class="ls-header"> | |
| <h1>Nova-1</h1> | |
| <div class="ls-sub">Standard Preview</div> | |
| </div> | |
| <button class="new-chat-btn" id="new-chat-btn"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> | |
| New Chat | |
| </button> | |
| <div class="ls-chats-label">Recent</div> | |
| <div class="ls-chat-item active" id="chat-item-0">New conversation</div> | |
| <div class="ls-footer">ZeroGPU ⚡ · Mixture of Depth</div> | |
| </div> | |
| <!-- MAIN CHAT --> | |
| <div id="chat-main"> | |
| <button class="toggle-btn" id="toggle-left" title="Toggle sidebar"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg> | |
| </button> | |
| <button class="toggle-btn" id="toggle-right" title="Toggle parameters"> | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4m0 14v4M4.22 4.22l2.83 2.83m9.9 9.9l2.83 2.83M1 12h4m14 0h4M4.22 19.78l2.83-2.83m9.9-9.9l2.83-2.83"/></svg> | |
| </button> | |
| <div id="chat-messages"> | |
| <div id="messages-inner" class="empty"> | |
| <div class="welcome-block"> | |
| <h2>How can I help you today?</h2> | |
| <p>Ask me anything — I'm running on ZeroGPU with Mixture of Depth for fast, intelligent responses.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="input-area"> | |
| <div id="input-inner"> | |
| <div id="input-row"> | |
| <textarea id="msg-input" placeholder="Message Nova…" rows="1"></textarea> | |
| <button id="send-btn"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg> | |
| </button> | |
| </div> | |
| <div class="input-hint">Nova can make mistakes. Verify important information.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT SIDEBAR --> | |
| <div class="sidebar" id="right-sidebar"> | |
| <div class="rs-header"><h2>Settings</h2></div> | |
| <div class="right-body"> | |
| <div class="rs-section"> | |
| <div class="rs-section-title">Context Window</div> | |
| <button id="yarn-btn"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 1-9 9m9-9a9 9 0 0 0-9-9m9 9H3m9 9a9 9 0 0 1-9-9m9 9c2.5 0 4.5-4 4.5-9S14.5 3 12 3m0 18c-2.5 0-4.5-4-4.5-9S9.5 3 12 3"/></svg> | |
| YaRN Stretch <span id="yarn-current" style="margin-left:auto">2K</span> | |
| </button> | |
| <div class="yarn-current" id="yarn-status">Native 2K</div> | |
| </div> | |
| <div class="rs-section"> | |
| <div class="rs-section-title">Generation</div> | |
| <div class="param-group"><label>Max Tokens <span class="param-val" id="val-max-tokens">512</span></label><input type="range" id="max-tokens" min="128" max="2048" step="64" value="512"></div> | |
| <div class="param-group"><label>Temperature <span class="param-val" id="val-temperature">0.6</span></label><input type="range" id="temperature" min="0.1" max="2.0" step="0.1" value="0.6"></div> | |
| <div class="param-group"><label>Top-P <span class="param-val" id="val-top-p">0.90</span></label><input type="range" id="top-p" min="0.1" max="1.0" step="0.05" value="0.9"></div> | |
| </div> | |
| <div class="rs-section"> | |
| <div class="rs-section-title">System Prompt</div> | |
| <textarea id="system-prompt" rows="4">You are Nova, a helpful, honest AI assistant. Answer questions clearly and concisely.</textarea> | |
| </div> | |
| <div class="rs-section"> | |
| <div class="rs-section-title">Mode</div> | |
| <div class="mode-group"> | |
| <button class="mode-btn active" id="mode-chat" data-mode="Chat">Chat</button> | |
| <button class="mode-btn" id="mode-base" data-mode="Base">Base</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- YaRN MODAL --> | |
| <div id="yarn-modal"> | |
| <div class="modal-card"> | |
| <h3>Context Window Stretching</h3> | |
| <div class="modal-desc">YaRN extrapolation extends the model's native 2K context to handle longer documents and conversations.</div> | |
| <div class="modal-warning">Stretching may slightly reduce precision on fine details. For most use cases, native 2K works best.</div> | |
| <div class="yarn-options" id="yarn-options"> | |
| <div class="yarn-option selected" data-ctx="2K (Native)" data-factor="1.0"><div class="y-label">2K</div><div class="y-factor">×1.0</div><div class="y-native">Native</div></div> | |
| <div class="yarn-option" data-ctx="4K (YaRN x2)" data-factor="2.0"><div class="y-label">4K</div><div class="y-factor">×2.0</div></div> | |
| <div class="yarn-option" data-ctx="8K (YaRN x4)" data-factor="4.0"><div class="y-label">8K</div><div class="y-factor">×4.0</div></div> | |
| <div class="yarn-option" data-ctx="16K (YaRN x8)" data-factor="8.0"><div class="y-label">16K</div><div class="y-factor">×8.0</div></div> | |
| </div> | |
| <div id="yarn-stretch-anim"> | |
| <div class="stretch-visual"> | |
| <svg class="helix-svg" viewBox="0 0 360 70"> | |
| <path class="helix-path" id="helix1" d="M20,15 C70,35 60,5 110,25 C160,45 150,15 200,35 C250,55 240,25 290,45 C320,57 340,35 340,35" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/> | |
| <path class="helix-path" id="helix2" d="M20,55 C70,35 60,65 110,45 C160,25 150,55 200,35 C250,15 240,45 290,25 C320,13 340,35 340,35" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/> | |
| <circle cx="20" cy="15" r="3" fill="currentColor" opacity="0.3"/> | |
| <circle cx="20" cy="55" r="3" fill="currentColor" opacity="0.3"/> | |
| <circle cx="340" cy="35" r="4" fill="currentColor" opacity="0.5" id="helix-dot"/> | |
| </svg> | |
| </div> | |
| <div class="stretch-bar-label"><span>2K</span><span id="stretch-target-label">2K</span></div> | |
| <div class="stretch-bar-wrap"><div class="stretch-bar" id="yarn-bar" style="width:0%"></div></div> | |
| <div class="stretch-info" id="yarn-anim-label">Ready</div> | |
| <div class="stretch-check" id="yarn-check">Context stretched successfully</div> | |
| </div> | |
| <div class="modal-buttons"> | |
| <button class="modal-cancel" id="yarn-cancel">Cancel</button> | |
| <button class="modal-apply" id="yarn-apply">Apply</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| // ── State ── | |
| let history = []; | |
| let isGenerating = false; | |
| let mode = "Chat"; | |
| let client = null; | |
| async function initClient() { | |
| try { | |
| client = await Client.connect(window.location.origin); | |
| console.log("✅ Connected to Gradio backend"); | |
| } catch (e) { | |
| console.error("❌ Failed to connect:", e); | |
| } | |
| } | |
| initClient(); | |
| // ── DOM refs ── | |
| const chatMessages = document.getElementById("chat-messages"); | |
| const messagesInner = document.getElementById("messages-inner"); | |
| const msgInput = document.getElementById("msg-input"); | |
| const sendBtn = document.getElementById("send-btn"); | |
| const newChatBtn = document.getElementById("new-chat-btn"); | |
| const inputArea = document.getElementById("input-area"); | |
| const maxTokensSlider = document.getElementById("max-tokens"); | |
| const temperatureSlider = document.getElementById("temperature"); | |
| const topPSlider = document.getElementById("top-p"); | |
| const systemPromptEl = document.getElementById("system-prompt"); | |
| const leftSidebar = document.getElementById("left-sidebar"); | |
| const rightSidebar = document.getElementById("right-sidebar"); | |
| const toggleLeft = document.getElementById("toggle-left"); | |
| const toggleRight = document.getElementById("toggle-right"); | |
| let leftOpen = true, rightOpen = true; | |
| toggleLeft.addEventListener("click", () => { | |
| leftOpen = !leftOpen; | |
| leftSidebar.classList.toggle("collapsed", !leftOpen); | |
| }); | |
| toggleRight.addEventListener("click", () => { | |
| rightOpen = !rightOpen; | |
| rightSidebar.classList.toggle("collapsed", !rightOpen); | |
| }); | |
| maxTokensSlider.addEventListener("input", () => { document.getElementById("val-max-tokens").textContent = maxTokensSlider.value; }); | |
| temperatureSlider.addEventListener("input", () => { document.getElementById("val-temperature").textContent = parseFloat(temperatureSlider.value).toFixed(1); }); | |
| topPSlider.addEventListener("input", () => { document.getElementById("val-top-p").textContent = parseFloat(topPSlider.value).toFixed(2); }); | |
| document.querySelectorAll(".mode-btn").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| document.querySelectorAll(".mode-btn").forEach(b => b.classList.remove("active")); | |
| btn.classList.add("active"); | |
| mode = btn.dataset.mode; | |
| }); | |
| }); | |
| msgInput.addEventListener("input", () => { | |
| msgInput.style.height = "auto"; | |
| msgInput.style.height = Math.min(msgInput.scrollHeight, 160) + "px"; | |
| }); | |
| function scrollDown() { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| // ── Layout helpers ── | |
| function enterChatMode() { | |
| messagesInner.classList.remove("empty"); | |
| const wb = messagesInner.querySelector(".welcome-block"); | |
| if (wb) wb.remove(); | |
| } | |
| function resetToEmpty() { | |
| messagesInner.classList.add("empty"); | |
| if (!messagesInner.querySelector(".welcome-block")) { | |
| const wb = document.createElement("div"); | |
| wb.className = "welcome-block"; | |
| wb.innerHTML = `<h2>How can I help you today?</h2><p>Ask me anything — I'm running on ZeroGPU with Mixture of Depth for fast, intelligent responses.</p>`; | |
| messagesInner.prepend(wb); | |
| } | |
| } | |
| // ── Escape HTML ── | |
| function escapeHtml(text) { | |
| const div = document.createElement("div"); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |
| // ── Action buttons (per message) ── | |
| function createMsgActions(msgIdx) { | |
| const actions = document.createElement("div"); | |
| actions.className = "msg-actions"; | |
| // Copy button | |
| const copyBtn = document.createElement("button"); | |
| copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy`; | |
| copyBtn.addEventListener("click", () => { | |
| navigator.clipboard.writeText(history[msgIdx]?.content || ""); | |
| }); | |
| // Regenerate button | |
| const reBtn = document.createElement("button"); | |
| reBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.5 17.5A9 9 0 1 0 2 12"/></svg> Regenerate`; | |
| reBtn.addEventListener("click", () => regenerate(msgIdx)); | |
| actions.appendChild(copyBtn); | |
| actions.appendChild(reBtn); | |
| return actions; | |
| } | |
| // ── Render messages ── | |
| function addUserMessage(text) { | |
| const div = document.createElement("div"); | |
| div.className = "msg-row user"; | |
| div.innerHTML = `<div class="bubble-user">${escapeHtml(text)}</div>`; | |
| messagesInner.appendChild(div); | |
| scrollDown(); | |
| } | |
| function addBotMessage(msgIdx) { | |
| const div = document.createElement("div"); | |
| div.className = "msg-row bot"; | |
| div.innerHTML = `<div class="bubble-bot"><div class="typing-indicator"><span></span><span></span><span></span></div></div>`; | |
| div.appendChild(createMsgActions(msgIdx)); | |
| messagesInner.appendChild(div); | |
| scrollDown(); | |
| return div; | |
| } | |
| function updateBotMessage(div, text) { | |
| const bubble = div.querySelector(".bubble-bot"); | |
| if (text) { | |
| bubble.innerHTML = escapeHtml(text).replace(/\n/g, "<br>"); | |
| } else { | |
| bubble.innerHTML = `<div class="typing-indicator"><span></span><span></span><span></span></div>`; | |
| } | |
| scrollDown(); | |
| } | |
| // ── Send message ── | |
| async function sendMessage() { | |
| if (isGenerating) return; | |
| const message = msgInput.value.trim(); | |
| if (!message) return; | |
| if (!client) { console.error("Client not connected"); return; } | |
| isGenerating = true; | |
| sendBtn.disabled = true; | |
| msgInput.value = ""; | |
| msgInput.style.height = "auto"; | |
| enterChatMode(); | |
| addUserMessage(message); | |
| history.push({ role: "user", content: message }); | |
| const assistantIdx = history.length; | |
| history.push({ role: "assistant", content: "" }); | |
| const botDiv = addBotMessage(assistantIdx); | |
| try { | |
| const priorHistory = history.slice(0, -2); | |
| const submission = client.submit("/generate", { | |
| message: message, | |
| history: priorHistory, | |
| system_prompt: systemPromptEl.value, | |
| max_tokens: parseInt(maxTokensSlider.value), | |
| temperature: parseFloat(temperatureSlider.value), | |
| top_p: parseFloat(topPSlider.value), | |
| mode: mode, | |
| }); | |
| let fullResponse = ""; | |
| for await (const msg of submission) { | |
| if (msg.type === "data") { | |
| let chunk = ""; | |
| if (Array.isArray(msg.data)) chunk = msg.data[0] ?? ""; | |
| else if (typeof msg.data === "string") chunk = msg.data; | |
| else if (msg.data && typeof msg.data === "object") chunk = msg.data[0] ?? msg.data ?? ""; | |
| if (chunk) { | |
| fullResponse += chunk; | |
| history[assistantIdx].content = fullResponse; | |
| updateBotMessage(botDiv, fullResponse); | |
| } | |
| } else if (msg.type === "status" && msg.status === "error") { | |
| fullResponse = "An error occurred during generation. Please try again."; | |
| history[assistantIdx].content = fullResponse; | |
| updateBotMessage(botDiv, fullResponse); | |
| break; | |
| } | |
| } | |
| if (!fullResponse) { | |
| fullResponse = "No response received."; | |
| history[assistantIdx].content = fullResponse; | |
| updateBotMessage(botDiv, fullResponse); | |
| } | |
| } catch (e) { | |
| console.error("Stream error:", e); | |
| const errText = "Connection error: " + e.message; | |
| history[assistantIdx].content = errText; | |
| updateBotMessage(botDiv, errText); | |
| } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| } | |
| // ── Regenerate ── | |
| async function regenerate(botIdx) { | |
| if (isGenerating) return; | |
| if (botIdx < 1 || history[botIdx - 1]?.role !== "user") return; | |
| const userMsg = history[botIdx - 1].content; | |
| // Remove the old bot message from DOM and history | |
| const botRow = messagesInner.querySelectorAll(".msg-row.bot")[ | |
| Array.from(messagesInner.querySelectorAll(".msg-row.bot")).findIndex(r => | |
| r.querySelector(".msg-actions") && r === [...messagesInner.querySelectorAll(".msg-row.bot")].find((_,i) => { | |
| // find by index match | |
| let botCount = 0; | |
| for (const child of messagesInner.children) { | |
| if (child.classList.contains("msg-row") && child.classList.contains("bot")) { | |
| if (botCount === (() => { let c = 0; for (const ch of messagesInner.children) { if (ch.classList.contains("msg-row") && ch.classList.contains("bot")) { if (ch === r) return c; c++; } } return -1; })()) return true; | |
| botCount++; | |
| } | |
| } | |
| return false; | |
| }) | |
| ) | |
| ]; | |
| // Simpler approach: just pop last assistant, find its row | |
| // Actually let's just remove the last bot row | |
| const allBotRows = messagesInner.querySelectorAll(".msg-row.bot"); | |
| const targetRow = allBotRows[allBotRows.length - 1]; | |
| history.pop(); // remove assistant | |
| if (targetRow) targetRow.remove(); | |
| const assistantIdx = history.length; | |
| history.push({ role: "assistant", content: "" }); | |
| const botDiv = addBotMessage(assistantIdx); | |
| isGenerating = true; | |
| sendBtn.disabled = true; | |
| try { | |
| const priorHistory = history.slice(0, -2); | |
| const submission = client.submit("/generate", { | |
| message: userMsg, | |
| history: priorHistory, | |
| system_prompt: systemPromptEl.value, | |
| max_tokens: parseInt(maxTokensSlider.value), | |
| temperature: parseFloat(temperatureSlider.value), | |
| top_p: parseFloat(topPSlider.value), | |
| mode: mode, | |
| }); | |
| let fullResponse = ""; | |
| for await (const msg of submission) { | |
| if (msg.type === "data") { | |
| let chunk = Array.isArray(msg.data) ? msg.data[0] : (typeof msg.data === "string" ? msg.data : ""); | |
| if (chunk) { fullResponse += chunk; history[assistantIdx].content = fullResponse; updateBotMessage(botDiv, fullResponse); } | |
| } | |
| } | |
| if (!fullResponse) updateBotMessage(botDiv, "No response received."); | |
| } catch (e) { updateBotMessage(botDiv, "Error: " + e.message); } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| } | |
| // ── Events ── | |
| sendBtn.addEventListener("click", sendMessage); | |
| msgInput.addEventListener("keydown", (e) => { | |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| }); | |
| newChatBtn.addEventListener("click", () => { | |
| history = []; | |
| messagesInner.innerHTML = ""; | |
| resetToEmpty(); | |
| }); | |
| // ── YaRN Modal ── | |
| const yarnModal = document.getElementById("yarn-modal"); | |
| const yarnBtn = document.getElementById("yarn-btn"); | |
| const yarnCancel = document.getElementById("yarn-cancel"); | |
| const yarnApply = document.getElementById("yarn-apply"); | |
| const yarnStretchAnim = document.getElementById("yarn-stretch-anim"); | |
| const yarnBar = document.getElementById("yarn-bar"); | |
| const yarnAnimLabel = document.getElementById("yarn-anim-label"); | |
| const yarnCheck = document.getElementById("yarn-check"); | |
| const yarnStatus = document.getElementById("yarn-status"); | |
| const yarnCurrentSpan = document.getElementById("yarn-current"); | |
| const stretchTargetLabel = document.getElementById("stretch-target-label"); | |
| const helix1 = document.getElementById("helix1"); | |
| const helix2 = document.getElementById("helix2"); | |
| let yarnSelectedCtx = "2K (Native)"; | |
| let yarnSelectedFactor = 1.0; | |
| let yarnSelectedK = "2K"; | |
| function resetYarnModal() { | |
| yarnStretchAnim.classList.remove("show"); | |
| yarnBar.style.width = "0%"; | |
| yarnBar.className = "stretch-bar"; | |
| yarnAnimLabel.textContent = "Ready"; | |
| yarnCheck.classList.remove("show"); | |
| yarnApply.disabled = false; | |
| helix1.classList.remove("active", "done"); | |
| helix2.classList.remove("active", "done"); | |
| document.querySelectorAll(".yarn-option").forEach(o => o.classList.remove("selected")); | |
| const nativeOpt = document.querySelector('.yarn-option[data-ctx="2K (Native)"]'); | |
| if (nativeOpt) nativeOpt.classList.add("selected"); | |
| yarnSelectedCtx = "2K (Native)"; | |
| yarnSelectedFactor = 1.0; | |
| yarnSelectedK = "2K"; | |
| stretchTargetLabel.textContent = "2K"; | |
| } | |
| yarnBtn.addEventListener("click", () => { resetYarnModal(); yarnModal.classList.add("show"); }); | |
| yarnCancel.addEventListener("click", () => { yarnModal.classList.remove("show"); }); | |
| yarnModal.addEventListener("click", (e) => { if (e.target === yarnModal) yarnModal.classList.remove("show"); }); | |
| document.querySelectorAll(".yarn-option").forEach(opt => { | |
| opt.addEventListener("click", () => { | |
| document.querySelectorAll(".yarn-option").forEach(o => o.classList.remove("selected")); | |
| opt.classList.add("selected"); | |
| yarnSelectedCtx = opt.dataset.ctx; | |
| yarnSelectedFactor = parseFloat(opt.dataset.factor); | |
| yarnSelectedK = opt.querySelector(".y-label").textContent; | |
| stretchTargetLabel.textContent = yarnSelectedK; | |
| }); | |
| }); | |
| yarnApply.addEventListener("click", async () => { | |
| yarnApply.disabled = true; | |
| yarnStretchAnim.classList.add("show"); | |
| yarnCheck.classList.remove("show"); | |
| helix1.classList.add("active"); | |
| helix2.classList.add("active"); | |
| if (yarnSelectedFactor === 1.0) { | |
| yarnBar.style.width = "100%"; | |
| yarnBar.className = "stretch-bar done"; | |
| helix1.classList.add("done"); helix1.classList.remove("active"); | |
| helix2.classList.add("done"); helix2.classList.remove("active"); | |
| yarnAnimLabel.textContent = "Native context — no stretching needed"; | |
| yarnCheck.classList.add("show"); | |
| try { | |
| const result = await client.predict("/apply_yarn", { target_context: yarnSelectedCtx }); | |
| const status = Array.isArray(result.data) ? result.data[0] : result.data; | |
| yarnStatus.textContent = "Native 2K"; | |
| yarnCurrentSpan.textContent = "2K"; | |
| } catch (e) { console.error("YaRN apply error:", e); } | |
| setTimeout(() => yarnModal.classList.remove("show"), 900); | |
| return; | |
| } | |
| const totalFrames = 60; | |
| const duration = 1800; | |
| const frameTime = duration / totalFrames; | |
| let frame = 0; | |
| const animInterval = setInterval(() => { | |
| frame++; | |
| const progress = frame / totalFrames; | |
| const eased = 1 - Math.pow(1 - progress, 3); | |
| const pct = Math.round(eased * 100); | |
| yarnBar.style.width = pct + "%"; | |
| yarnBar.className = "stretch-bar active"; | |
| yarnAnimLabel.textContent = `Stretching to ${yarnSelectedK}… ${pct}%`; | |
| helix1.style.strokeDasharray = (eased * 300) + " 300"; | |
| helix2.style.strokeDasharray = (eased * 300) + " 300"; | |
| if (frame >= totalFrames) { | |
| clearInterval(animInterval); | |
| yarnBar.className = "stretch-bar done"; | |
| helix1.classList.add("done"); helix1.classList.remove("active"); | |
| helix2.classList.add("done"); helix2.classList.remove("active"); | |
| helix1.style.strokeDasharray = "300 0"; | |
| helix2.style.strokeDasharray = "300 0"; | |
| (async () => { | |
| try { | |
| const result = await client.predict("/apply_yarn", { target_context: yarnSelectedCtx }); | |
| const status = Array.isArray(result.data) ? result.data[0] : result.data; | |
| yarnAnimLabel.textContent = status || `Context: ${yarnSelectedK}`; | |
| yarnStatus.textContent = status || `${yarnSelectedK} active`; | |
| yarnCurrentSpan.textContent = yarnSelectedK; | |
| } catch (e) { yarnAnimLabel.textContent = "Error applying"; console.error("YaRN error:", e); } | |
| yarnCheck.classList.add("show"); | |
| setTimeout(() => yarnModal.classList.remove("show"), 1200); | |
| })(); | |
| } | |
| }, frameTime); | |
| }); | |
| </script> | |
| </body> | |
| </html> |