Nemotron-3-Nano-Omni / index.html
akhaliq's picture
akhaliq HF Staff
feat: add completion signal to stream response and update frontend to handle termination
7aeff6e
<!DOCTYPE html>
<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>