Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Nemotron-3 Nano Omni — NVIDIA</title> | |
| <meta name="description" content="Chat with NVIDIA Nemotron-3 Nano Omni — 30B-A3B multimodal reasoning model for enterprise agent systems"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| :root{ | |
| --bg:#0d0d0d; | |
| --msg-user:#0d0d0d; | |
| --msg-ai:#171717; | |
| --surface:#1a1a1a; | |
| --surface2:#212121; | |
| --surface3:#2a2a2a; | |
| --border:#2e2e2e; | |
| --border-light:#3a3a3a; | |
| --text:#ececec; | |
| --text-secondary:#a0a0a0; | |
| --text-dim:#6b6b6b; | |
| --nvidia:#76b900; | |
| --nvidia-light:#8ccf0a; | |
| --nvidia-dark:#5a8f00; | |
| --nvidia-glow:#76b90018; | |
| --nvidia-glow2:#76b90030; | |
| --think-bg:#111a08; | |
| --think-border:#76b90025; | |
| --error:#ef4444; | |
| --code-bg:#0a0a0a; | |
| } | |
| html,body{height:100%;font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased} | |
| /* ── App Shell ── */ | |
| .app{display:flex;flex-direction:column;height:100vh;position:relative} | |
| /* ── Top Bar ── */ | |
| .topbar{ | |
| display:flex;align-items:center;justify-content:center;gap:10px; | |
| padding:14px 20px;border-bottom:1px solid var(--border); | |
| background:var(--bg);flex-shrink:0;position:relative;z-index:10; | |
| } | |
| .topbar-logo{ | |
| width:28px;height:28px;border-radius:8px; | |
| background:linear-gradient(135deg,#76b900,#5a9400); | |
| display:flex;align-items:center;justify-content:center; | |
| box-shadow:0 2px 12px #76b90030; | |
| } | |
| .topbar-logo svg{width:16px;height:16px} | |
| .topbar-title{font-size:.92rem;font-weight:600;color:var(--text);letter-spacing:-.01em} | |
| .topbar-badge{ | |
| font-size:.6rem;font-weight:600;letter-spacing:.05em;text-transform:uppercase; | |
| padding:3px 8px;border-radius:5px; | |
| background:var(--nvidia-glow2);color:var(--nvidia);border:1px solid #76b90025; | |
| } | |
| .topbar-controls{position:absolute;right:16px;display:flex;gap:8px;align-items:center} | |
| .topbar-controls button{ | |
| background:none;border:1px solid var(--border);color:var(--text-secondary); | |
| padding:5px 12px;border-radius:8px;cursor:pointer;font-size:.7rem; | |
| font-family:'Inter',sans-serif;font-weight:500;transition:.2s; | |
| } | |
| .topbar-controls button:hover{border-color:var(--border-light);color:var(--text)} | |
| .topbar-controls .clear-btn:hover{border-color:var(--error);color:var(--error)} | |
| /* ── Settings Drawer ── */ | |
| .settings-drawer{ | |
| display:none;border-bottom:1px solid var(--border); | |
| padding:12px 20px;background:var(--surface); | |
| justify-content:center;gap:24px;flex-wrap:wrap;flex-shrink:0; | |
| } | |
| .settings-drawer.open{display:flex} | |
| .setting{display:flex;align-items:center;gap:8px} | |
| .setting label{font-size:.72rem;color:var(--text-secondary);white-space:nowrap;font-weight:500} | |
| .setting input[type=range]{ | |
| width:100px;height:4px;accent-color:var(--nvidia);cursor:pointer; | |
| -webkit-appearance:none;appearance:none;background:var(--surface3);border-radius:2px; | |
| } | |
| .setting input[type=range]::-webkit-slider-thumb{ | |
| -webkit-appearance:none;width:14px;height:14px;border-radius:50%; | |
| background:var(--nvidia);cursor:pointer;border:2px solid var(--bg); | |
| } | |
| .setting span{ | |
| font-size:.72rem;color:var(--nvidia);font-family:'JetBrains Mono',monospace; | |
| min-width:34px;font-weight:500; | |
| } | |
| /* ── Messages ── */ | |
| .messages{flex:1;overflow-y:auto;scroll-behavior:smooth} | |
| .messages::-webkit-scrollbar{width:6px} | |
| .messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px} | |
| .messages::-webkit-scrollbar-thumb:hover{background:var(--border-light)} | |
| .msg-row{ | |
| padding:24px 0;animation:msgIn .4s cubic-bezier(.22,1,.36,1); | |
| border-bottom:1px solid #1a1a1a; | |
| } | |
| .msg-row.ai{background:var(--msg-ai)} | |
| .msg-row.user{background:var(--msg-user)} | |
| @keyframes msgIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} | |
| .msg-inner{ | |
| max-width:720px;margin:0 auto;padding:0 24px; | |
| display:flex;gap:16px;align-items:flex-start; | |
| } | |
| .msg-icon{ | |
| width:30px;height:30px;border-radius:8px;flex-shrink:0; | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:.72rem;font-weight:700;margin-top:2px; | |
| } | |
| .msg-row.user .msg-icon{background:var(--surface3);color:var(--text-secondary)} | |
| .msg-row.ai .msg-icon{ | |
| background:linear-gradient(135deg,#76b900,#5a9400);color:#fff; | |
| box-shadow:0 2px 8px #76b90020; | |
| } | |
| .msg-label{font-size:.72rem;font-weight:600;margin-bottom:6px;letter-spacing:.01em} | |
| .msg-row.user .msg-label{color:var(--text-secondary)} | |
| .msg-row.ai .msg-label{color:var(--nvidia)} | |
| .msg-body{flex:1;min-width:0} | |
| .msg-content{font-size:.9rem;line-height:1.8;color:var(--text);word-wrap:break-word;overflow-wrap:break-word} | |
| /* ── Reasoning Block ── */ | |
| .reasoning-block{ | |
| margin-bottom:14px;border-radius:10px;overflow:hidden; | |
| border:1px solid var(--think-border);background:var(--think-bg); | |
| transition:all .3s ease; | |
| } | |
| .reasoning-header{ | |
| display:flex;align-items:center;gap:8px;padding:10px 14px; | |
| cursor:pointer;user-select:none;transition:.15s; | |
| } | |
| .reasoning-header:hover{background:#76b90008} | |
| .reasoning-icon{ | |
| width:20px;height:20px;border-radius:5px; | |
| background:var(--nvidia-glow2); | |
| display:flex;align-items:center;justify-content:center; | |
| font-size:.6rem;color:var(--nvidia); | |
| } | |
| .reasoning-label{font-size:.72rem;font-weight:600;color:var(--nvidia);letter-spacing:.01em} | |
| .reasoning-chevron{ | |
| margin-left:auto;font-size:.65rem;color:var(--nvidia); | |
| transition:transform .3s ease;opacity:.6; | |
| } | |
| .reasoning-block.open .reasoning-chevron{transform:rotate(180deg)} | |
| .reasoning-body{ | |
| max-height:0;overflow:hidden;transition:max-height .4s cubic-bezier(.22,1,.36,1); | |
| } | |
| .reasoning-block.open .reasoning-body{max-height:500px} | |
| .reasoning-text{ | |
| padding:0 14px 14px;font-size:.8rem;line-height:1.7; | |
| color:var(--text-secondary);white-space:pre-wrap; | |
| max-height:350px;overflow-y:auto; | |
| } | |
| .reasoning-text::-webkit-scrollbar{width:3px} | |
| .reasoning-text::-webkit-scrollbar-thumb{background:#76b90030;border-radius:2px} | |
| /* ── Code ── */ | |
| .msg-content pre{ | |
| background:var(--code-bg);border:1px solid var(--border);border-radius:10px; | |
| padding:16px;margin:12px 0;overflow-x:auto; | |
| font-family:'JetBrains Mono',monospace;font-size:.8rem;line-height:1.65; | |
| } | |
| .msg-content code{ | |
| font-family:'JetBrains Mono',monospace;font-size:.84em; | |
| background:var(--surface3);padding:2px 7px;border-radius:5px; | |
| } | |
| .msg-content pre code{background:none;padding:0;border-radius:0} | |
| .msg-content p{margin:6px 0} | |
| .msg-content ul,.msg-content ol{margin:8px 0 8px 20px} | |
| .msg-content li{margin:4px 0} | |
| .msg-content strong{font-weight:600;color:#fff} | |
| /* ── Typing Indicator ── */ | |
| .typing-dots{display:flex;gap:5px;padding:6px 0;align-items:center} | |
| .typing-dots span{ | |
| width:7px;height:7px;border-radius:50%; | |
| background:var(--nvidia);animation:dotPulse 1.4s infinite;opacity:.4; | |
| } | |
| .typing-dots span:nth-child(2){animation-delay:.15s} | |
| .typing-dots span:nth-child(3){animation-delay:.3s} | |
| @keyframes dotPulse{ | |
| 0%,100%{opacity:.2;transform:scale(.8)} | |
| 50%{opacity:.7;transform:scale(1)} | |
| } | |
| /* ── Input Area ── */ | |
| .input-area{ | |
| padding:20px 24px 28px;flex-shrink:0; | |
| background:linear-gradient(180deg,transparent,var(--bg) 30%); | |
| position:relative; | |
| } | |
| .input-area::before{ | |
| content:'';position:absolute;top:-40px;left:0;right:0;height:40px; | |
| background:linear-gradient(180deg,transparent,var(--bg));pointer-events:none; | |
| } | |
| .input-container{max-width:720px;margin:0 auto} | |
| .input-box{ | |
| display:flex;align-items:flex-end;gap:0; | |
| background:var(--surface2);border:1px solid var(--border); | |
| border-radius:20px;padding:8px 8px 8px 20px; | |
| transition:border-color .25s,box-shadow .25s; | |
| } | |
| .input-box:focus-within{ | |
| border-color:var(--nvidia); | |
| box-shadow:0 0 0 2px var(--nvidia-glow),0 4px 24px #76b90010; | |
| } | |
| #userInput{ | |
| flex:1;background:none;border:none;color:var(--text); | |
| font-size:.9rem;font-family:'Inter',sans-serif; | |
| resize:none;outline:none;max-height:160px;min-height:26px; | |
| line-height:1.55;padding:6px 0; | |
| } | |
| #userInput::placeholder{color:var(--text-dim)} | |
| #sendBtn{ | |
| width:38px;height:38px;border-radius:12px;border:none; | |
| background:var(--nvidia);color:#fff;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center; | |
| transition:all .2s;flex-shrink:0; | |
| } | |
| #sendBtn:hover{background:var(--nvidia-light);transform:scale(1.05);box-shadow:0 4px 16px #76b90030} | |
| #sendBtn:disabled{background:var(--surface3);color:var(--text-dim);cursor:not-allowed;transform:none;box-shadow:none} | |
| #sendBtn svg{width:16px;height:16px} | |
| .input-footer{ | |
| text-align:center;margin-top:10px;font-size:.65rem;color:var(--text-dim); | |
| } | |
| /* ── Attach Button ── */ | |
| #attachBtn{ | |
| width:38px;height:38px;border-radius:12px;border:none; | |
| background:none;color:var(--text-secondary);cursor:pointer; | |
| display:flex;align-items:center;justify-content:center; | |
| transition:all .2s;flex-shrink:0;margin-right:4px; | |
| } | |
| #attachBtn:hover{color:var(--nvidia);background:var(--nvidia-glow2)} | |
| #attachBtn svg{width:18px;height:18px} | |
| /* ── File Preview Strip ── */ | |
| .attach-previews{ | |
| display:flex;gap:8px;padding:0 20px 8px;flex-wrap:wrap; | |
| max-width:720px;margin:0 auto; | |
| } | |
| .attach-previews:empty{display:none} | |
| .attach-preview{ | |
| position:relative;border-radius:10px;overflow:hidden; | |
| border:1px solid var(--border);background:var(--surface); | |
| animation:msgIn .3s ease; | |
| } | |
| .attach-preview img,.attach-preview video{ | |
| height:72px;max-width:140px;object-fit:cover;display:block;border-radius:9px; | |
| } | |
| .attach-preview .audio-tag{ | |
| display:flex;align-items:center;gap:8px;padding:10px 14px; | |
| font-size:.72rem;color:var(--text-secondary); | |
| } | |
| .attach-preview .audio-tag svg{width:18px;height:18px;color:var(--nvidia);flex-shrink:0} | |
| .attach-preview .audio-name{max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} | |
| .attach-remove{ | |
| position:absolute;top:3px;right:3px;width:20px;height:20px; | |
| border-radius:50%;background:rgba(0,0,0,.7);border:none; | |
| color:#fff;font-size:.65rem;cursor:pointer; | |
| display:flex;align-items:center;justify-content:center; | |
| opacity:0;transition:.15s; | |
| } | |
| .attach-preview:hover .attach-remove{opacity:1} | |
| /* ── Media in Messages ── */ | |
| .msg-media{margin:8px 0 4px;display:flex;flex-wrap:wrap;gap:8px} | |
| .msg-media img{max-width:320px;max-height:240px;border-radius:10px;border:1px solid var(--border)} | |
| .msg-media video{max-width:360px;max-height:260px;border-radius:10px;border:1px solid var(--border)} | |
| .msg-media audio{max-width:300px;margin:4px 0} | |
| .msg-media .media-label{ | |
| font-size:.65rem;color:var(--text-dim);margin-top:2px; | |
| display:flex;align-items:center;gap:4px; | |
| } | |
| /* ── Welcome Screen ── */ | |
| .welcome{ | |
| display:flex;flex-direction:column;align-items:center;justify-content:center; | |
| flex:1;padding:60px 24px;animation:fadeUp .6s ease; | |
| } | |
| @keyframes fadeUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:none}} | |
| .welcome-logo{ | |
| width:64px;height:64px;border-radius:18px; | |
| background:linear-gradient(135deg,#76b900,#5a9400); | |
| display:flex;align-items:center;justify-content:center; | |
| box-shadow:0 8px 40px #76b90025;margin-bottom:24px; | |
| } | |
| .welcome-logo svg{width:34px;height:34px} | |
| .welcome h2{font-size:1.5rem;font-weight:600;color:var(--text);margin-bottom:8px;letter-spacing:-.02em} | |
| .welcome p{font-size:.85rem;color:var(--text-secondary);max-width:480px;text-align:center;line-height:1.7;margin-bottom:28px} | |
| .prompt-grid{ | |
| display:grid;grid-template-columns:repeat(2,1fr);gap:10px; | |
| max-width:520px;width:100%; | |
| } | |
| .prompt-card{ | |
| padding:14px 16px;border-radius:12px;cursor:pointer; | |
| background:var(--surface);border:1px solid var(--border); | |
| transition:all .2s;text-align:left; | |
| } | |
| .prompt-card:hover{border-color:var(--nvidia);background:var(--surface2);transform:translateY(-2px);box-shadow:0 4px 20px #76b90010} | |
| .prompt-card .pc-icon{font-size:.9rem;margin-bottom:6px} | |
| .prompt-card .pc-text{font-size:.78rem;color:var(--text-secondary);line-height:1.5} | |
| .prompt-card:hover .pc-text{color:var(--text)} | |
| /* ── Visual Examples ── */ | |
| .visual-examples{ | |
| display:flex;gap:12px;margin-top:24px;margin-bottom:12px; | |
| max-width:680px;width:100%;justify-content:center;flex-wrap:wrap; | |
| } | |
| .visual-card{ | |
| position:relative;width:150px;height:200px; | |
| border-radius:14px;overflow:hidden;cursor:pointer; | |
| background-size:cover;background-position:center; | |
| box-shadow:0 4px 12px rgba(0,0,0,0.2); | |
| transition:transform .25s,box-shadow .25s; | |
| } | |
| .visual-card:hover{ | |
| transform:translateY(-4px); | |
| box-shadow:0 8px 24px rgba(118,185,0,0.2); | |
| } | |
| .visual-card::after{ | |
| content:'';position:absolute;inset:0; | |
| background:linear-gradient(180deg,transparent 40%,rgba(0,0,0,0.9) 100%); | |
| } | |
| .visual-card .vc-icon{ | |
| position:absolute;top:10px;right:10px; | |
| background:rgba(0,0,0,0.6);backdrop-filter:blur(4px); | |
| padding:4px 6px;border-radius:6px;font-size:.8rem;z-index:2; | |
| } | |
| .visual-card .vc-text{ | |
| position:absolute;bottom:14px;left:14px;right:14px; | |
| color:#fff;font-size:.85rem;font-weight:600;z-index:2; | |
| text-shadow:0 1px 4px rgba(0,0,0,0.8);line-height:1.3; | |
| } | |
| /* ── Responsive ── */ | |
| @media(max-width:640px){ | |
| .msg-inner{padding:0 16px;gap:12px} | |
| .prompt-grid{grid-template-columns:1fr} | |
| .topbar-controls{position:static;margin-left:auto} | |
| .topbar{gap:8px;padding:12px 16px} | |
| .input-area{padding:16px 16px 20px} | |
| .welcome{padding:40px 20px} | |
| .welcome h2{font-size:1.25rem} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <!-- Top Bar --> | |
| <div class="topbar"> | |
| <div class="topbar-logo"> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#fff"/></svg> | |
| </div> | |
| <span class="topbar-title">Nemotron-3 Nano Omni</span> | |
| <span class="topbar-badge">30B-A3B · Multimodal</span> | |
| <div class="topbar-controls"> | |
| <button id="settingsBtn" onclick="document.querySelector('.settings-drawer').classList.toggle('open')">⚙ Settings</button> | |
| <button class="clear-btn" id="clearBtn">✕ New Chat</button> | |
| </div> | |
| </div> | |
| <!-- Settings Drawer --> | |
| <div class="settings-drawer" id="settingsDrawer"> | |
| <div class="setting"> | |
| <label>Temperature</label> | |
| <input type="range" id="tempSlider" min="0.1" max="1.5" step="0.1" value="0.6"> | |
| <span id="tempVal">0.6</span> | |
| </div> | |
| <div class="setting"> | |
| <label>Max Tokens</label> | |
| <input type="range" id="tokenSlider" min="256" max="8192" step="256" value="4096"> | |
| <span id="tokenVal">4096</span> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div class="messages" id="messages"> | |
| <div class="welcome" id="welcomeScreen"> | |
| <div class="welcome-logo"> | |
| <svg viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#fff"/></svg> | |
| </div> | |
| <h2>Nemotron-3 Nano Omni</h2> | |
| <p>NVIDIA's 30B-A3B open multimodal model designed as a perception and context sub-agent for enterprise agent systems. Accepts text, image, video, and audio inputs with built-in chain-of-thought reasoning.</p> | |
| <div class="visual-examples"> | |
| <div class="visual-card" style="background-image:url('https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg', 'Cat03.jpg', 'image', 'Describe this image in detail.')"> | |
| <div class="vc-icon">🖼️</div> | |
| <div class="vc-text">Image Analysis</div> | |
| </div> | |
| <div class="visual-card" style="background-image:url('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Big_Buck_Bunny_4K.webm/640px-Big_Buck_Bunny_4K.webm.jpg')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/transcoded/c/c0/Big_Buck_Bunny_4K.webm/Big_Buck_Bunny_4K.webm.240p.vp9.webm', 'Big_Buck_Bunny.webm', 'video', 'What is happening in this video?')"> | |
| <div class="vc-icon">🎬</div> | |
| <div class="vc-text">Video Summary</div> | |
| </div> | |
| <div class="visual-card" style="background-image:url('https://images.unsplash.com/photo-1612831455359-970e23a1e4e9?auto=format&fit=crop&q=80&w=400')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg', 'Example.ogg', 'audio', 'What is in this audio?')"> | |
| <div class="vc-icon">🎵</div> | |
| <div class="vc-text">Audio Transcript</div> | |
| </div> | |
| </div> | |
| <div class="prompt-grid"> | |
| <div class="prompt-card" onclick="fillPrompt(this)"> | |
| <div class="pc-icon">🧮</div> | |
| <div class="pc-text">How many r's are in the word 'strawberry'?</div> | |
| </div> | |
| <div class="prompt-card" onclick="fillPrompt(this)"> | |
| <div class="pc-icon">🤖</div> | |
| <div class="pc-text">What makes a good perception sub-agent in an enterprise AI system?</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input --> | |
| <div class="input-area"> | |
| <div class="input-container"> | |
| <div class="attach-previews" id="attachPreviews"></div> | |
| <div class="input-box"> | |
| <button id="attachBtn" title="Attach image, audio, or video"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/> | |
| </svg> | |
| </button> | |
| <input type="file" id="fileInput" accept="image/*,audio/*,video/*" multiple hidden> | |
| <textarea id="userInput" rows="1" placeholder="Message Nemotron…" autofocus></textarea> | |
| <button id="sendBtn"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 12h14M12 5l7 7-7 7"/> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="input-footer">Nemotron-3 Nano Omni · 30B-A3B Multimodal · NVIDIA · Powered by OpenRouter</div> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| const messagesEl = document.getElementById('messages'); | |
| const userInput = document.getElementById('userInput'); | |
| const sendBtn = document.getElementById('sendBtn'); | |
| const tempSlider = document.getElementById('tempSlider'); | |
| const tempVal = document.getElementById('tempVal'); | |
| const tokenSlider = document.getElementById('tokenSlider'); | |
| const tokenVal = document.getElementById('tokenVal'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| const attachBtn = document.getElementById('attachBtn'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const attachPreviews = document.getElementById('attachPreviews'); | |
| let history = []; | |
| let client = null; | |
| let isGenerating = false; | |
| let attachedFiles = []; // {file, type: 'image'|'audio'|'video', dataUrl, name} | |
| async function initClient() { | |
| client = await Client.connect(window.location.origin); | |
| } | |
| initClient(); | |
| tempSlider.oninput = () => tempVal.textContent = tempSlider.value; | |
| tokenSlider.oninput = () => tokenVal.textContent = tokenSlider.value; | |
| // Expose fillPrompt globally | |
| window.fillPrompt = function(card) { | |
| const text = card.querySelector('.pc-text').textContent; | |
| userInput.value = text; | |
| userInput.focus(); | |
| userInput.dispatchEvent(new Event('input')); | |
| sendMessage(); | |
| }; | |
| window.loadExample = async function(url, filename, type, promptText) { | |
| try { | |
| const card = event.currentTarget; | |
| const originalBtnText = card.innerHTML; | |
| card.innerHTML = '<div class="pc-icon">⏳</div><div class="pc-text">Loading...</div>'; | |
| const resp = await fetch(url); | |
| if (!resp.ok) throw new Error('Failed to load example'); | |
| const blob = await resp.blob(); | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| attachedFiles.push({ file: new File([blob], filename, {type: blob.type}), type: type, dataUrl: e.target.result, name: filename }); | |
| renderPreviews(); | |
| card.innerHTML = originalBtnText; | |
| if (promptText) { | |
| userInput.value = promptText; | |
| userInput.dispatchEvent(new Event('input')); | |
| sendMessage(); | |
| } | |
| }; | |
| reader.readAsDataURL(blob); | |
| } catch (err) { | |
| console.error(err); | |
| alert('Could not load example media.'); | |
| } | |
| }; | |
| // ── Attach file handling ── | |
| attachBtn.onclick = () => fileInput.click(); | |
| fileInput.onchange = () => { | |
| for (const file of fileInput.files) { | |
| const mediaType = file.type.startsWith('image/') ? 'image' | |
| : file.type.startsWith('audio/') ? 'audio' | |
| : file.type.startsWith('video/') ? 'video' : null; | |
| if (!mediaType) continue; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const dataUrl = e.target.result; | |
| const entry = { file, type: mediaType, dataUrl, name: file.name }; | |
| attachedFiles.push(entry); | |
| renderPreviews(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| fileInput.value = ''; | |
| }; | |
| function renderPreviews() { | |
| attachPreviews.innerHTML = ''; | |
| attachedFiles.forEach((entry, idx) => { | |
| const el = document.createElement('div'); | |
| el.className = 'attach-preview'; | |
| if (entry.type === 'image') { | |
| el.innerHTML = `<img src="${entry.dataUrl}" alt="${escapeHtml(entry.name)}"><button class="attach-remove" data-idx="${idx}">✕</button>`; | |
| } else if (entry.type === 'video') { | |
| el.innerHTML = `<video src="${entry.dataUrl}" muted></video><button class="attach-remove" data-idx="${idx}">✕</button>`; | |
| } else { | |
| el.innerHTML = `<div class="audio-tag"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg><span class="audio-name">${escapeHtml(entry.name)}</span></div><button class="attach-remove" data-idx="${idx}">✕</button>`; | |
| } | |
| attachPreviews.appendChild(el); | |
| }); | |
| attachPreviews.querySelectorAll('.attach-remove').forEach(btn => { | |
| btn.onclick = (e) => { | |
| e.stopPropagation(); | |
| attachedFiles.splice(parseInt(btn.dataset.idx), 1); | |
| renderPreviews(); | |
| }; | |
| }); | |
| } | |
| // ── Clear ── | |
| clearBtn.onclick = () => { | |
| history = []; | |
| attachedFiles = []; | |
| renderPreviews(); | |
| messagesEl.innerHTML = ''; | |
| const welcome = document.createElement('div'); | |
| welcome.className = 'welcome'; | |
| welcome.id = 'welcomeScreen'; | |
| welcome.innerHTML = ` | |
| <div class="welcome-logo"><svg viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#fff"/></svg></div> | |
| <h2>Nemotron-3 Nano Omni</h2> | |
| <p>NVIDIA's 30B-A3B open multimodal model designed as a perception and context sub-agent for enterprise agent systems. Accepts text, image, video, and audio inputs with built-in chain-of-thought reasoning.</p> | |
| <div class="visual-examples"> | |
| <div class="visual-card" style="background-image:url('https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg', 'Cat03.jpg', 'image', 'Describe this image in detail.')"> | |
| <div class="vc-icon">🖼️</div> | |
| <div class="vc-text">Image Analysis</div> | |
| </div> | |
| <div class="visual-card" style="background-image:url('https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Big_Buck_Bunny_4K.webm/640px-Big_Buck_Bunny_4K.webm.jpg')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/transcoded/c/c0/Big_Buck_Bunny_4K.webm/Big_Buck_Bunny_4K.webm.240p.vp9.webm', 'Big_Buck_Bunny.webm', 'video', 'What is happening in this video?')"> | |
| <div class="vc-icon">🎬</div> | |
| <div class="vc-text">Video Summary</div> | |
| </div> | |
| <div class="visual-card" style="background-image:url('https://images.unsplash.com/photo-1612831455359-970e23a1e4e9?auto=format&fit=crop&q=80&w=400')" onclick="loadExample('https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg', 'Example.ogg', 'audio', 'What is in this audio?')"> | |
| <div class="vc-icon">🎵</div> | |
| <div class="vc-text">Audio Transcript</div> | |
| </div> | |
| </div> | |
| <div class="prompt-grid"> | |
| <div class="prompt-card" onclick="fillPrompt(this)"> | |
| <div class="pc-icon">🧮</div> | |
| <div class="pc-text">How many r's are in the word 'strawberry'?</div> | |
| </div> | |
| <div class="prompt-card" onclick="fillPrompt(this)"> | |
| <div class="pc-icon">🤖</div> | |
| <div class="pc-text">What makes a good perception sub-agent in an enterprise AI system?</div> | |
| </div> | |
| </div>`; | |
| messagesEl.appendChild(welcome); | |
| }; | |
| userInput.addEventListener('input', () => { | |
| userInput.style.height = 'auto'; | |
| userInput.style.height = Math.min(userInput.scrollHeight, 160) + 'px'; | |
| }); | |
| userInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } | |
| }); | |
| sendBtn.onclick = sendMessage; | |
| // ── Drag & Drop ── | |
| const inputBox = document.querySelector('.input-box'); | |
| ['dragenter','dragover'].forEach(ev => { | |
| inputBox.addEventListener(ev, (e) => { e.preventDefault(); inputBox.style.borderColor = 'var(--nvidia)'; }); | |
| }); | |
| ['dragleave','drop'].forEach(ev => { | |
| inputBox.addEventListener(ev, (e) => { e.preventDefault(); inputBox.style.borderColor = ''; }); | |
| }); | |
| inputBox.addEventListener('drop', (e) => { | |
| for (const file of e.dataTransfer.files) { | |
| const mediaType = file.type.startsWith('image/') ? 'image' | |
| : file.type.startsWith('audio/') ? 'audio' | |
| : file.type.startsWith('video/') ? 'video' : null; | |
| if (!mediaType) continue; | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| attachedFiles.push({ file, type: mediaType, dataUrl: ev.target.result, name: file.name }); | |
| renderPreviews(); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| function removeWelcome() { | |
| const w = document.getElementById('welcomeScreen'); | |
| if (w) w.remove(); | |
| } | |
| function buildMediaHtml(mediaItems) { | |
| if (!mediaItems || mediaItems.length === 0) return ''; | |
| let html = '<div class="msg-media">'; | |
| for (const m of mediaItems) { | |
| if (m.type === 'image') { | |
| html += `<img src="${m.dataUrl}" alt="Uploaded image">`; | |
| } else if (m.type === 'video') { | |
| html += `<video src="${m.dataUrl}" controls muted preload="metadata"></video>`; | |
| } else if (m.type === 'audio') { | |
| html += `<audio src="${m.dataUrl}" controls preload="metadata"></audio>`; | |
| } | |
| } | |
| html += '</div>'; | |
| return html; | |
| } | |
| function addMessage(role, content, reasoning, mediaItems) { | |
| removeWelcome(); | |
| const row = document.createElement('div'); | |
| row.className = `msg-row ${role === 'user' ? 'user' : 'ai'}`; | |
| const iconContent = role === 'user' | |
| ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>' | |
| : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#fff"/></svg>'; | |
| const label = role === 'user' ? 'You' : 'Nemotron'; | |
| let reasoningHtml = ''; | |
| if (role !== 'user' && reasoning) { | |
| const reasoningText = typeof reasoning === 'string' ? reasoning : JSON.stringify(reasoning, null, 2); | |
| reasoningHtml = ` | |
| <div class="reasoning-block" onclick="this.classList.toggle('open')"> | |
| <div class="reasoning-header"> | |
| <div class="reasoning-icon">⚡</div> | |
| <span class="reasoning-label">Reasoning</span> | |
| <span class="reasoning-chevron">▼</span> | |
| </div> | |
| <div class="reasoning-body"> | |
| <div class="reasoning-text">${escapeHtml(reasoningText)}</div> | |
| </div> | |
| </div>`; | |
| } | |
| const mediaHtml = (role === 'user' && mediaItems) ? buildMediaHtml(mediaItems) : ''; | |
| row.innerHTML = ` | |
| <div class="msg-inner"> | |
| <div class="msg-icon">${iconContent}</div> | |
| <div class="msg-body"> | |
| <div class="msg-label">${label}</div> | |
| ${mediaHtml} | |
| ${reasoningHtml} | |
| <div class="msg-content">${formatContent(content, role)}</div> | |
| </div> | |
| </div>`; | |
| messagesEl.appendChild(row); | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| return row; | |
| } | |
| function addTypingIndicator() { | |
| removeWelcome(); | |
| const row = document.createElement('div'); | |
| row.className = 'msg-row ai'; | |
| row.id = 'typing'; | |
| row.innerHTML = ` | |
| <div class="msg-inner"> | |
| <div class="msg-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#fff"/></svg></div> | |
| <div class="msg-body"> | |
| <div class="msg-label">Nemotron</div> | |
| <div class="msg-content"><div class="typing-dots"><span></span><span></span><span></span></div></div> | |
| </div> | |
| </div>`; | |
| messagesEl.appendChild(row); | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| } | |
| function removeTypingIndicator() { | |
| const el = document.getElementById('typing'); | |
| if (el) el.remove(); | |
| } | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const d = document.createElement('div'); | |
| d.textContent = text; | |
| return d.innerHTML; | |
| } | |
| function formatContent(text, role) { | |
| if (!text) return '<span style="color:var(--text-dim);font-style:italic">No response content</span>'; | |
| if (role === 'user') return escapeHtml(text); | |
| let formatted = escapeHtml(text); | |
| formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); | |
| formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); | |
| formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>'); | |
| formatted = formatted.replace(/\n/g, '<br>'); | |
| return formatted; | |
| } | |
| function getMimePrefix(type) { | |
| if (type === 'image') return 'image_url'; | |
| if (type === 'audio') return 'audio_url'; | |
| if (type === 'video') return 'video_url'; | |
| return null; | |
| } | |
| async function sendMessage() { | |
| const text = userInput.value.trim(); | |
| if ((!text && attachedFiles.length === 0) || isGenerating || !client) return; | |
| isGenerating = true; | |
| sendBtn.disabled = true; | |
| userInput.disabled = true; | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| // Capture current attachments and clear | |
| const currentMedia = [...attachedFiles]; | |
| attachedFiles = []; | |
| renderPreviews(); | |
| // Show user message with media previews | |
| addMessage('user', text, null, currentMedia); | |
| addTypingIndicator(); | |
| // Build multimodal content parts | |
| const contentParts = []; | |
| for (const m of currentMedia) { | |
| const urlKey = getMimePrefix(m.type); | |
| if (urlKey) { | |
| contentParts.push({ type: urlKey, [urlKey]: { url: m.dataUrl } }); | |
| } | |
| } | |
| if (text) { | |
| contentParts.push({ type: "text", text: text }); | |
| } | |
| try { | |
| const job = client.submit("/chat", { | |
| message: contentParts, | |
| history: history, | |
| max_tokens: parseInt(tokenSlider.value), | |
| temperature: parseFloat(tempSlider.value), | |
| enable_thinking: false, | |
| }); | |
| removeTypingIndicator(); | |
| let assistantRow = null; | |
| let finalContent = ''; | |
| let finalReasoning = null; | |
| for await (const msg of job) { | |
| if (msg.type === "status") { | |
| if (msg.stage === "complete") { | |
| break; | |
| } | |
| if (msg.stage === "error") { | |
| throw new Error(msg.message || "Generation failed"); | |
| } | |
| } | |
| if (!msg.data) continue; | |
| const data = msg.data[0]; | |
| finalContent = data.content || ''; | |
| finalReasoning = data.reasoning || null; | |
| if (!assistantRow) { | |
| assistantRow = addMessage('assistant', finalContent, finalReasoning); | |
| } else { | |
| let reasoningHtml = ''; | |
| if (finalReasoning) { | |
| const reasoningText = typeof finalReasoning === 'string' ? finalReasoning : JSON.stringify(finalReasoning, null, 2); | |
| reasoningHtml = ` | |
| <div class="reasoning-block" onclick="this.classList.toggle('open')"> | |
| <div class="reasoning-header"> | |
| <div class="reasoning-icon">⚡</div> | |
| <span class="reasoning-label">Reasoning</span> | |
| <span class="reasoning-chevron">▼</span> | |
| </div> | |
| <div class="reasoning-body"> | |
| <div class="reasoning-text">${escapeHtml(reasoningText)}</div> | |
| </div> | |
| </div>`; | |
| } | |
| const msgBody = assistantRow.querySelector('.msg-body'); | |
| const wasOpen = msgBody.querySelector('.reasoning-block.open') !== null; | |
| if (wasOpen && finalReasoning) { | |
| reasoningHtml = reasoningHtml.replace('class="reasoning-block"', 'class="reasoning-block open"'); | |
| } | |
| const labelHtml = '<div class="msg-label">Nemotron</div>'; | |
| const contentHtml = `<div class="msg-content">${formatContent(finalContent, 'assistant')}</div>`; | |
| msgBody.innerHTML = labelHtml + reasoningHtml + contentHtml; | |
| messagesEl.scrollTop = messagesEl.scrollHeight; | |
| } | |
| if (data.done === true) { | |
| break; | |
| } | |
| } | |
| const historyEntry = { role: "assistant", content: finalContent }; | |
| if (finalReasoning) historyEntry.reasoning_details = finalReasoning; | |
| history.push({ role: "user", content: text || "[media attached]" }); | |
| history.push(historyEntry); | |
| } catch (err) { | |
| removeTypingIndicator(); | |
| addMessage('assistant', `Error: ${err.message}`); | |
| } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| userInput.disabled = false; | |
| userInput.focus(); | |
| } | |
| </script> | |
| </body> | |
| </html> | |