akhaliq's picture
akhaliq HF Staff
fix: Transition to cumulative yielding in Python backend and replacement rendering in JS frontend to prevent token duplication
f63b065
Raw
History Blame Contribute Delete
60.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NVIDIA Nemotron 3 Ultra 550B - Chat Assistant</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
<!-- Markdown Parser & Code Syntax Highlighter CDNs -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css">
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/highlight.min.js"></script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- App Styling -->
<style>
:root {
/* Space Dark Theme (Default) */
--bg-gradient: radial-gradient(circle at 50% 50%, #0d0e15 0%, #050608 100%);
--sidebar-bg: rgba(13, 14, 21, 0.45);
--card-bg: rgba(20, 22, 33, 0.45);
--card-border: rgba(99, 102, 241, 0.15);
--card-border-hover: rgba(99, 102, 241, 0.35);
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--primary: #6366f1;
--primary-glow: rgba(99, 102, 241, 0.35);
--secondary: #a855f7;
--accent: #10b981;
--user-bubble-bg: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);
--bot-bubble-bg: rgba(30, 32, 50, 0.5);
--input-bg: rgba(15, 17, 28, 0.6);
--glow-color: #6366f1;
--font-main: 'Outfit', sans-serif;
--font-mono: 'Fira Code', monospace;
--sidebar-width: 280px;
--settings-width: 320px;
}
/* Cyber Glow Theme */
[data-theme="cyber"] {
--bg-gradient: radial-gradient(circle at 50% 50%, #0a0c10 0%, #020304 100%);
--sidebar-bg: rgba(10, 12, 16, 0.6);
--card-bg: rgba(15, 18, 25, 0.5);
--card-border: rgba(6, 182, 212, 0.2);
--card-border-hover: rgba(236, 72, 153, 0.4);
--text-main: #e2e8f0;
--text-muted: #64748b;
--primary: #06b6d4;
--primary-glow: rgba(6, 182, 212, 0.4);
--secondary: #ec4899;
--accent: #f59e0b;
--user-bubble-bg: linear-gradient(135deg, #0891b2 0%, #0369a1 100%);
--bot-bubble-bg: rgba(21, 26, 38, 0.6);
--input-bg: rgba(11, 14, 20, 0.8);
--glow-color: #06b6d4;
}
/* Emerald Glass Theme */
[data-theme="emerald"] {
--bg-gradient: radial-gradient(circle at 50% 50%, #061512 0%, #020706 100%);
--sidebar-bg: rgba(6, 21, 18, 0.55);
--card-bg: rgba(10, 31, 27, 0.45);
--card-border: rgba(16, 185, 129, 0.15);
--card-border-hover: rgba(16, 185, 129, 0.35);
--text-main: #f0fdf4;
--text-muted: #86efac;
--primary: #10b981;
--primary-glow: rgba(16, 185, 129, 0.35);
--secondary: #14b8a6;
--accent: #fbbf24;
--user-bubble-bg: linear-gradient(135deg, #059669 0%, #047857 100%);
--bot-bubble-bg: rgba(15, 45, 39, 0.5);
--input-bg: rgba(7, 24, 20, 0.7);
--glow-color: #10b981;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
}
body {
font-family: var(--font-main);
background: var(--bg-gradient);
color: var(--text-main);
height: 100vh;
display: flex;
overflow: hidden;
letter-spacing: -0.01em;
}
/* Layout */
.sidebar {
width: var(--sidebar-width);
height: 100%;
background: var(--sidebar-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-right: 1px solid var(--card-border);
display: flex;
flex-direction: column;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 10;
}
.chat-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
background: transparent;
}
/* Sidebar Header */
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-container {
display: flex;
align-items: center;
gap: 10px;
}
.logo-icon {
color: var(--primary);
filter: drop-shadow(0 0 8px var(--primary-glow));
}
.logo-text {
font-size: 1.1rem;
font-weight: 700;
background: linear-gradient(135deg, var(--text-main) 30%, var(--primary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.new-chat-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: var(--text-main);
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
}
.new-chat-btn:hover {
background: var(--primary-glow);
border-color: var(--primary);
box-shadow: 0 0 12px var(--primary-glow);
}
/* Sessions List */
.sessions-list {
flex: 1;
overflow-y: auto;
padding: 15px 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.session-item {
padding: 12px;
border-radius: 12px;
border: 1px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
background: transparent;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.session-item:hover {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.05);
}
.session-item.active {
background: var(--card-bg);
border-color: var(--card-border);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.session-item.active::before {
content: '';
position: absolute;
left: 0;
top: 15%;
height: 70%;
width: 4px;
background: var(--primary);
border-radius: 0 4px 4px 0;
box-shadow: 0 0 8px var(--primary-glow);
}
.session-info {
display: flex;
flex-direction: column;
gap: 4px;
width: calc(100% - 30px);
}
.session-title {
font-size: 0.9rem;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-subtitle {
font-size: 0.75rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-delete-btn {
background: transparent;
border: none;
color: var(--text-muted);
opacity: 0;
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.2s ease;
}
.session-item:hover .session-delete-btn {
opacity: 1;
}
.session-delete-btn:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/* Sidebar Footer */
.sidebar-footer {
padding: 15px;
border-top: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.theme-select-container {
display: flex;
align-items: center;
gap: 8px;
}
.theme-select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: var(--text-main);
padding: 6px 10px;
border-radius: 8px;
font-size: 0.8rem;
font-family: var(--font-main);
cursor: pointer;
outline: none;
}
/* Chat Header */
.chat-header {
height: 70px;
border-bottom: 1px solid var(--card-border);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 0 25px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 5;
}
.chat-title-container {
display: flex;
flex-direction: column;
}
.chat-title {
font-size: 1rem;
font-weight: 600;
}
.chat-status {
font-size: 0.75rem;
color: var(--accent);
display: flex;
align-items: center;
gap: 5px;
}
.status-dot {
width: 6px;
height: 6px;
background-color: var(--accent);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent);
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.header-btn {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--card-border);
color: var(--text-main);
width: 38px;
height: 38px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.header-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: var(--primary);
box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);
}
/* Messages Area */
.messages-viewport {
flex: 1;
overflow-y: auto;
padding: 30px 20px;
display: flex;
flex-direction: column;
gap: 25px;
}
.message-row {
display: flex;
width: 100%;
animation: fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.message-row.user {
justify-content: flex-end;
}
.message-row.assistant {
justify-content: flex-start;
}
.message-bubble {
max-width: 75%;
padding: 16px 20px;
border-radius: 20px;
font-size: 0.95rem;
line-height: 1.6;
word-wrap: break-word;
position: relative;
}
.message-row.user .message-bubble {
background: var(--user-bubble-bg);
color: #ffffff;
border-bottom-right-radius: 4px;
box-shadow: 0 4px 15px rgba(79, 70, 229, 0.25);
}
.message-row.assistant .message-bubble {
background: var(--bot-bubble-bg);
color: var(--text-main);
border-bottom-left-radius: 4px;
border: 1px solid var(--card-border);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
}
/* Markdown Styles inside message bubbles */
.message-bubble p {
margin-bottom: 12px;
}
.message-bubble p:last-child {
margin-bottom: 0;
}
.message-bubble pre {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 14px;
margin: 12px 0;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.05);
position: relative;
}
.message-bubble code {
font-family: var(--font-mono);
font-size: 0.85rem;
background: rgba(255, 255, 255, 0.08);
padding: 2px 5px;
border-radius: 4px;
}
.message-bubble pre code {
background: transparent;
padding: 0;
border-radius: 0;
}
/* Copy Button for code blocks */
.copy-code-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: var(--text-muted);
padding: 4px 8px;
font-size: 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
opacity: 0;
transition: all 0.2s ease;
}
.message-bubble pre:hover .copy-code-btn {
opacity: 1;
}
.copy-code-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: var(--text-main);
}
.message-bubble table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 0.9rem;
}
.message-bubble th, .message-bubble td {
border: 1px solid var(--card-border);
padding: 8px 12px;
text-align: left;
}
.message-bubble th {
background: rgba(255, 255, 255, 0.05);
}
.message-bubble ul, .message-bubble ol {
margin-left: 20px;
margin-bottom: 12px;
}
/* Empty State */
.empty-chat-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
padding: 40px;
gap: 20px;
animation: fadeIn 0.6s ease;
}
.empty-icon-box {
width: 80px;
height: 80px;
border-radius: 24px;
background: rgba(99, 102, 241, 0.06);
border: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--primary);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
position: relative;
}
.empty-icon-box::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border-radius: 24px;
box-shadow: 0 0 20px var(--primary-glow);
opacity: 0.5;
}
.empty-chat-state h2 {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 8px;
}
.empty-chat-state p {
color: var(--text-muted);
max-width: 480px;
font-size: 0.95rem;
line-height: 1.5;
}
.suggestions-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
max-width: 540px;
margin-top: 15px;
}
.suggestion-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 15px;
border-radius: 14px;
cursor: pointer;
text-align: left;
transition: all 0.2s ease;
}
.suggestion-card:hover {
border-color: var(--primary);
background: rgba(255, 255, 255, 0.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.suggestion-card-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 4px;
}
.suggestion-card-desc {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Input Area */
.chat-input-area {
padding: 20px 25px 30px;
background: transparent;
z-index: 5;
}
.input-wrapper {
background: var(--input-bg);
border: 1px solid var(--card-border);
border-radius: 18px;
padding: 10px 14px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: all 0.2s ease;
}
.input-wrapper:focus-within {
border-color: var(--primary);
box-shadow: 0 0 15px var(--primary-glow), 0 10px 30px rgba(0, 0, 0, 0.25);
}
.chat-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text-main);
font-family: var(--font-main);
font-size: 0.95rem;
resize: none;
max-height: 120px;
min-height: 24px;
padding: 6px 0;
}
.chat-input::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
.send-btn {
background: var(--primary);
border: none;
color: #ffffff;
width: 36px;
height: 36px;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 10px var(--primary-glow);
}
.send-btn:hover {
box-shadow: 0 0 15px var(--glow-color);
transform: scale(1.05);
}
.send-btn:disabled {
background: rgba(255, 255, 255, 0.05);
color: var(--text-muted);
cursor: not-allowed;
box-shadow: none;
transform: none;
}
/* Settings Panel Drawer */
.settings-panel {
width: var(--settings-width);
height: 100%;
background: var(--sidebar-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-left: 1px solid var(--card-border);
display: flex;
flex-direction: column;
position: absolute;
right: 0;
top: 0;
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 9;
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.25);
}
.settings-panel.open {
transform: translateX(0);
}
.settings-header {
padding: 20px;
border-bottom: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.settings-title {
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.settings-body {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 22px;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.setting-label {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-main);
display: flex;
justify-content: space-between;
align-items: center;
}
.setting-value-display {
font-size: 0.8rem;
color: var(--primary);
font-family: var(--font-mono);
font-weight: 500;
}
.setting-input {
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--card-border);
color: var(--text-main);
padding: 10px 12px;
border-radius: 10px;
outline: none;
font-size: 0.9rem;
font-family: var(--font-main);
transition: all 0.2s ease;
}
.setting-input:focus {
border-color: var(--primary);
box-shadow: 0 0 8px var(--primary-glow);
}
.setting-textarea {
min-height: 80px;
max-height: 180px;
resize: vertical;
}
.slider-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.setting-slider {
flex: 1;
height: 5px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
outline: none;
-webkit-appearance: none;
accent-color: var(--primary);
}
.token-input-container {
display: flex;
position: relative;
}
.token-input-container input {
width: 100%;
padding-right: 38px;
}
.token-toggle-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
}
.token-save-btn {
background: var(--primary);
border: none;
color: #ffffff;
padding: 10px;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: all 0.2s ease;
text-align: center;
box-shadow: 0 4px 10px var(--primary-glow);
}
.token-save-btn:hover {
box-shadow: 0 0 15px var(--glow-color);
}
/* Token missing Banner */
.token-warning-banner {
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 12px;
padding: 12px 16px;
display: flex;
align-items: flex-start;
gap: 12px;
margin: 20px 20px 0;
animation: fadeIn 0.4s ease;
}
.token-warning-banner-text {
font-size: 0.85rem;
color: #fbd38d;
line-height: 1.4;
}
.token-warning-banner-link {
color: #f6ad55;
text-decoration: underline;
cursor: pointer;
font-weight: 600;
}
/* Typing indicator */
.typing-indicator {
display: flex;
gap: 6px;
align-items: center;
height: 20px;
padding-left: 4px;
}
.typing-dot {
width: 7px;
height: 7px;
background-color: var(--text-muted);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
position: absolute;
left: -100%;
top: 0;
height: 100%;
z-index: 10;
}
.sidebar.mobile-open {
left: 0;
}
.message-bubble {
max-width: 85%;
}
}
</style>
</head>
<body>
<!-- Sidebar: Sessions History -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo-container">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="logo-icon"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275Z"/></svg>
<span class="logo-text">Nemotron 3</span>
</div>
<button class="new-chat-btn" id="new-chat-btn">
<i data-lucide="plus" width="14" height="14"></i>
New Chat
</button>
</div>
<!-- Sessions List -->
<div class="sessions-list" id="sessions-list">
<!-- Dynamic elements loaded via JS -->
</div>
<!-- Sidebar Footer & Theme Toggle -->
<div class="sidebar-footer">
<div class="theme-select-container">
<i data-lucide="palette" width="16" height="16" style="color: var(--primary);"></i>
<select class="theme-select" id="theme-select">
<option value="default">Space Dark</option>
<option value="cyber">Cyber Glow</option>
<option value="emerald">Emerald Glass</option>
</select>
</div>
</div>
</aside>
<!-- Main Chat Workspace -->
<main class="chat-container">
<!-- Header -->
<header class="chat-header">
<div class="chat-title-container">
<div class="chat-title">Nemotron-3-Ultra-550B</div>
<div class="chat-status">
<span class="status-dot"></span>
<span>Ready</span>
</div>
</div>
<div class="header-actions">
<button class="header-btn" id="sidebar-toggle-btn" style="display: none;">
<i data-lucide="menu" width="18" height="18"></i>
</button>
<button class="header-btn" id="settings-toggle-btn">
<i data-lucide="settings" width="18" height="18"></i>
</button>
</div>
</header>
<!-- Warning banner if token is missing -->
<div class="token-warning-banner" id="token-warning-banner" style="display: none;">
<i data-lucide="key-round" width="18" height="18" style="color: #fbbf24; flex-shrink: 0;"></i>
<div class="token-warning-banner-text">
Hugging Face Token is not set. The assistant will not be able to answer.
Please enter a token in the <span class="token-warning-banner-link" id="token-banner-link">Settings panel</span>.
</div>
</div>
<!-- Message Viewport -->
<div class="messages-viewport" id="messages-viewport">
<!-- Empty state or chat bubbles loaded via JS -->
</div>
<!-- Message Input Area -->
<div class="chat-input-area">
<div class="input-wrapper">
<textarea class="chat-input" id="chat-input" rows="1" placeholder="Type a message..."></textarea>
<button class="send-btn" id="send-btn" disabled>
<i data-lucide="arrow-up" width="18" height="18"></i>
</button>
</div>
</div>
<!-- Settings drawer panel -->
<div class="settings-panel" id="settings-panel">
<div class="settings-header">
<div class="settings-title">
<i data-lucide="sliders" width="18" height="18" style="color: var(--primary);"></i>
Model Parameters
</div>
<button class="header-btn" id="settings-close-btn">
<i data-lucide="x" width="18" height="18"></i>
</button>
</div>
<div class="settings-body">
<!-- System Prompt -->
<div class="setting-group">
<label class="setting-label" for="setting-system-prompt">System Prompt</label>
<textarea class="setting-input setting-textarea" id="setting-system-prompt" placeholder="Custom instructions for the model..."></textarea>
</div>
<!-- Temperature Slider -->
<div class="setting-group">
<div class="setting-label">
<span>Temperature</span>
<span class="setting-value-display" id="temp-display">0.7</span>
</div>
<div class="slider-wrapper">
<input type="range" class="setting-slider" id="setting-temp" min="0" max="1.5" step="0.1" value="0.7">
</div>
</div>
<!-- Max Tokens Slider -->
<div class="setting-group">
<div class="setting-label">
<span>Max Tokens</span>
<span class="setting-value-display" id="max-tokens-display">1024</span>
</div>
<div class="slider-wrapper">
<input type="range" class="setting-slider" id="setting-max-tokens" min="64" max="4096" step="64" value="1024">
</div>
</div>
<!-- Custom Hugging Face Token -->
<div class="setting-group" style="border-top: 1px solid var(--card-border); padding-top: 15px; margin-top: 10px;">
<label class="setting-label">HF Token (Optional Override)</label>
<div class="token-input-container">
<input type="password" class="setting-input" id="setting-hf-token" placeholder="hf_...">
<button class="token-toggle-btn" id="token-toggle-btn" type="button">
<i data-lucide="eye" width="16" height="16"></i>
</button>
</div>
<button class="token-save-btn" id="token-save-btn">Save Token Locally</button>
</div>
</div>
</div>
</main>
<!-- Gradio Client and Main App Script -->
<script type="module">
import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
// Global State variables
let sessions = [];
let activeSessionId = null;
let isConfigTokenAvailable = false;
let gradioClient = null;
const suggestions = [
{
title: "Explain AI Concepts",
desc: "SFT vs RLHF...",
prompt: "Explain the difference between supervised fine-tuning (SFT) and reinforcement learning from human feedback (RLHF)."
},
{
title: "Code Generation",
desc: "Parallel image loading in Python...",
prompt: "Write a fast Python script to parallelize image loading using threads."
},
{
title: "Design a Plan",
desc: "Marketing strategy for a SaaS tool...",
prompt: "Outline a comprehensive marketing strategy to launch a new SaaS developer tool."
},
{
title: "Code Review",
desc: "Find optimization bottlenecks...",
prompt: "Review this code snippet for potential performance bottlenecks:\n\n```python\nfor i in range(len(list)):\n if list[i] in other_list: pass\n```"
}
];
// Dom elements
const chatInput = document.getElementById("chat-input");
const sendBtn = document.getElementById("send-btn");
const messagesViewport = document.getElementById("messages-viewport");
const sessionsList = document.getElementById("sessions-list");
const newChatBtn = document.getElementById("new-chat-btn");
const themeSelect = document.getElementById("theme-select");
const settingsToggleBtn = document.getElementById("settings-toggle-btn");
const settingsCloseBtn = document.getElementById("settings-close-btn");
const settingsPanel = document.getElementById("settings-panel");
const tokenWarningBanner = document.getElementById("token-warning-banner");
const tokenBannerLink = document.getElementById("token-banner-link");
// Parameter elements
const settingSystemPrompt = document.getElementById("setting-system-prompt");
const settingTemp = document.getElementById("setting-temp");
const tempDisplay = document.getElementById("temp-display");
const settingMaxTokens = document.getElementById("setting-max-tokens");
const maxTokensDisplay = document.getElementById("max-tokens-display");
const settingHfToken = document.getElementById("setting-hf-token");
const tokenToggleBtn = document.getElementById("token-toggle-btn");
const tokenSaveBtn = document.getElementById("token-save-btn");
// Initialization
async function init() {
try {
// 1. Load saved settings from localStorage
loadLocalSettings();
// 2. Load sessions from localStorage
loadSessionsFromStorage();
// 3. Set up event listeners immediately so the UI remains interactive
setupEventListeners();
// 4. Render initial UI states
renderSessions();
if (activeSessionId) {
renderActiveChat();
} else {
renderEmptyState();
}
// 5. Create Lucide icons safely
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
} catch (e) {
console.error("Error during UI initialization:", e);
}
// 6. Connect to Gradio Backend (Asynchronously, non-blocking)
connectGradioClient();
// 7. Check server configuration (Asynchronously, non-blocking)
checkServerConfig();
}
// Async Gradio connection
async function connectGradioClient() {
try {
console.log("Connecting to Gradio backend...");
gradioClient = await client(window.location.origin);
console.log("Gradio client connected successfully!");
} catch (err) {
console.error("Failed to connect to Gradio backend:", err);
}
}
// Check if HF_TOKEN is in server env
async function checkServerConfig() {
try {
const res = await fetch("/hf_token_status");
if (res.ok) {
const config = await res.json();
isConfigTokenAvailable = config.has_token;
} else {
isConfigTokenAvailable = false;
}
} catch (e) {
console.error("Error reading token status endpoint:", e);
isConfigTokenAvailable = false;
}
updateTokenWarningVisibility();
}
// Show/Hide token warning banner based on config availability or local storage override
function updateTokenWarningVisibility() {
const hasLocalToken = !!localStorage.getItem("hf_custom_token");
if (isConfigTokenAvailable || hasLocalToken) {
tokenWarningBanner.style.display = "none";
} else {
tokenWarningBanner.style.display = "flex";
}
}
// Local Storage settings loading
function loadLocalSettings() {
// Theme selection
const savedTheme = localStorage.getItem("app_theme") || "default";
if (themeSelect) {
themeSelect.value = savedTheme;
}
if (savedTheme !== "default") {
document.body.setAttribute("data-theme", savedTheme);
}
// System prompt
const savedSystemPrompt = localStorage.getItem("model_system_prompt") ||
"You are a helpful, respectful, and honest assistant powered by the NVIDIA Nemotron-3-Ultra-550B model. Always answer as helpfully and accurately as possible.";
if (settingSystemPrompt) {
settingSystemPrompt.value = savedSystemPrompt;
}
// Temperature
const savedTemp = localStorage.getItem("model_temperature") || "0.7";
if (settingTemp) {
settingTemp.value = savedTemp;
}
if (tempDisplay) {
tempDisplay.textContent = savedTemp;
}
// Max Tokens
const savedMaxTokens = localStorage.getItem("model_max_tokens") || "1024";
if (settingMaxTokens) {
settingMaxTokens.value = savedMaxTokens;
}
if (maxTokensDisplay) {
maxTokensDisplay.textContent = savedMaxTokens;
}
// Local Token
const savedLocalToken = localStorage.getItem("hf_custom_token") || "";
if (settingHfToken) {
settingHfToken.value = savedLocalToken;
}
}
// Load sessions list from localStorage
function loadSessionsFromStorage() {
try {
const rawSessions = localStorage.getItem("chat_sessions");
if (rawSessions) {
const parsed = JSON.parse(rawSessions);
if (Array.isArray(parsed)) {
sessions = parsed;
} else {
sessions = [];
}
} else {
sessions = [];
}
const rawActiveId = localStorage.getItem("active_session_id");
if (rawActiveId && sessions.some(s => s.id === rawActiveId)) {
activeSessionId = rawActiveId;
} else if (sessions.length > 0) {
activeSessionId = sessions[0].id;
} else {
activeSessionId = null;
}
} catch (e) {
console.error("Error loading sessions from storage:", e);
sessions = [];
activeSessionId = null;
}
// Create a default session if list is empty
if (sessions.length === 0) {
createNewSession();
}
}
function saveSessionsToStorage() {
localStorage.setItem("chat_sessions", JSON.stringify(sessions));
if (activeSessionId) {
localStorage.setItem("active_session_id", activeSessionId);
} else {
localStorage.removeItem("active_session_id");
}
}
function createNewSession() {
const newSession = {
id: 'sess_' + Date.now(),
title: 'New Chat session',
messages: [],
timestamp: new Date().toLocaleString()
};
sessions.unshift(newSession);
activeSessionId = newSession.id;
saveSessionsToStorage();
}
// Render Sidebar Sessions List
function renderSessions() {
if (!sessionsList) return;
sessionsList.innerHTML = "";
sessions.forEach(sess => {
const item = document.createElement("div");
item.className = `session-item ${sess.id === activeSessionId ? 'active' : ''}`;
item.dataset.id = sess.id;
const lastMsg = (sess.messages && sess.messages.length > 0)
? sess.messages[sess.messages.length - 1].content
: "No messages yet";
item.innerHTML = `
<div class="session-info">
<div class="session-title">${escapeHtml(sess.title)}</div>
<div class="session-subtitle">${escapeHtml(lastMsg)}</div>
</div>
<button class="session-delete-btn" title="Delete conversation">
<i data-lucide="trash-2" width="14" height="14"></i>
</button>
`;
// Add delete handler
const delBtn = item.querySelector(".session-delete-btn");
if (delBtn) {
delBtn.addEventListener("click", (e) => {
e.stopPropagation();
deleteSession(sess.id);
});
}
// Select handler
item.addEventListener("click", () => {
selectSession(sess.id);
});
sessionsList.appendChild(item);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
function selectSession(id) {
activeSessionId = id;
saveSessionsToStorage();
renderSessions();
renderActiveChat();
}
function deleteSession(id) {
sessions = sessions.filter(s => s.id !== id);
if (activeSessionId === id) {
activeSessionId = sessions.length > 0 ? sessions[0].id : null;
}
if (sessions.length === 0) {
createNewSession();
}
saveSessionsToStorage();
renderSessions();
renderActiveChat();
}
// Render Active Chat Bubbles
function renderActiveChat() {
if (!messagesViewport) return;
const currentSession = sessions.find(s => s.id === activeSessionId);
if (!currentSession || !currentSession.messages || currentSession.messages.length === 0) {
renderEmptyState();
return;
}
messagesViewport.innerHTML = "";
currentSession.messages.forEach(msg => {
appendBubbleToViewport(msg.role, msg.content);
});
scrollToBottom();
}
// Render Empty state when there are no messages
function renderEmptyState() {
if (!messagesViewport) return;
// Build suggestions cards HTML dynamically to avoid nested quotes or escaping bugs
let cardsHtml = "";
suggestions.forEach((item, index) => {
cardsHtml += `
<div class="suggestion-card" data-index="${index}">
<div class="suggestion-card-title">${escapeHtml(item.title)}</div>
<div class="suggestion-card-desc">${escapeHtml(item.desc)}</div>
</div>
`;
});
messagesViewport.innerHTML = `
<div class="empty-chat-state">
<div class="empty-icon-box">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div>
<h2>NVIDIA Nemotron 3 Ultra Chat</h2>
<p>Ask anything! This model is highly tuned for complex instructions, coding help, and deep reasoning. Powered by NVIDIA's 550B Ultra architecture.</p>
</div>
<div class="suggestions-grid">
${cardsHtml}
</div>
</div>
`;
// Suggestion click listeners
document.querySelectorAll(".suggestion-card").forEach(card => {
card.addEventListener("click", () => {
const idx = parseInt(card.dataset.index);
const prompt = suggestions[idx].prompt;
if (chatInput) {
chatInput.value = prompt;
adjustTextareaHeight();
chatInput.focus();
}
validateInput();
});
});
}
// Appending bubbles
function appendBubbleToViewport(role, content) {
if (!messagesViewport) return null;
const row = document.createElement("div");
row.className = `message-row ${role}`;
const bubble = document.createElement("div");
bubble.className = "message-bubble";
if (role === "user") {
bubble.textContent = content;
} else {
bubble.innerHTML = parseMarkdown(content);
addCopyButtons(bubble);
}
row.appendChild(bubble);
messagesViewport.appendChild(row);
return bubble;
}
// Parse Markdown safely
function parseMarkdown(text) {
try {
if (typeof marked !== 'undefined') {
return marked.parse(text);
}
return escapeHtml(text).replace(/\n/g, "<br>");
} catch (e) {
return escapeHtml(text).replace(/\n/g, "<br>");
}
}
// Add copy code triggers to <pre> blocks
function addCopyButtons(container) {
container.querySelectorAll("pre").forEach(pre => {
if (pre.querySelector(".copy-code-btn")) return;
const btn = document.createElement("button");
btn.className = "copy-code-btn";
btn.innerHTML = `Copy`;
if (typeof lucide !== 'undefined') {
btn.innerHTML = `<i data-lucide="copy" width="12" height="12"></i> Copy`;
}
btn.addEventListener("click", () => {
const code = pre.querySelector("code")?.textContent || pre.textContent;
navigator.clipboard.writeText(code).then(() => {
btn.innerHTML = `Copied!`;
if (typeof lucide !== 'undefined') {
btn.innerHTML = `<i data-lucide="check" width="12" height="12" style="color: var(--accent);"></i> Copied!`;
}
setTimeout(() => {
btn.innerHTML = `Copy`;
if (typeof lucide !== 'undefined') {
btn.innerHTML = `<i data-lucide="copy" width="12" height="12"></i> Copy`;
lucide.createIcons();
}
}, 2000);
});
});
pre.appendChild(btn);
// Highlight block
const codeBlock = pre.querySelector("code");
if (codeBlock && typeof hljs !== 'undefined') {
hljs.highlightElement(codeBlock);
}
});
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
// Event listener setup
function setupEventListeners() {
// Typing input auto height & validation
if (chatInput) {
chatInput.addEventListener("input", () => {
adjustTextareaHeight();
validateInput();
});
// Enter key to submit (Shift+Enter for newline)
chatInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (sendBtn && !sendBtn.disabled) {
sendMessage();
}
}
});
}
// Send trigger
if (sendBtn) {
sendBtn.addEventListener("click", sendMessage);
}
// New session
if (newChatBtn) {
newChatBtn.addEventListener("click", () => {
createNewSession();
renderSessions();
renderActiveChat();
if (chatInput) {
chatInput.focus();
}
});
}
// Theme selector change
if (themeSelect) {
themeSelect.addEventListener("change", (e) => {
const val = e.target.value;
localStorage.setItem("app_theme", val);
if (val === "default") {
document.body.removeAttribute("data-theme");
} else {
document.body.setAttribute("data-theme", val);
}
});
}
// Settings Toggle
if (settingsToggleBtn) {
settingsToggleBtn.addEventListener("click", () => {
if (settingsPanel) settingsPanel.classList.toggle("open");
});
}
if (settingsCloseBtn) {
settingsCloseBtn.addEventListener("click", () => {
if (settingsPanel) settingsPanel.classList.remove("open");
});
}
if (tokenBannerLink) {
tokenBannerLink.addEventListener("click", () => {
if (settingsPanel) settingsPanel.classList.add("open");
if (settingHfToken) settingHfToken.focus();
});
}
// Parameter updates listeners
if (settingTemp) {
settingTemp.addEventListener("input", (e) => {
const val = e.target.value;
if (tempDisplay) tempDisplay.textContent = val;
localStorage.setItem("model_temperature", val);
});
}
if (settingMaxTokens) {
settingMaxTokens.addEventListener("input", (e) => {
const val = e.target.value;
if (maxTokensDisplay) maxTokensDisplay.textContent = val;
localStorage.setItem("model_max_tokens", val);
});
}
if (settingSystemPrompt) {
settingSystemPrompt.addEventListener("change", (e) => {
localStorage.setItem("model_system_prompt", e.target.value);
});
}
// Save Token trigger
if (tokenSaveBtn) {
tokenSaveBtn.addEventListener("click", () => {
if (settingHfToken) {
const tok = settingHfToken.value.trim();
if (tok) {
localStorage.setItem("hf_custom_token", tok);
alert("Token saved locally in browser storage!");
} else {
localStorage.removeItem("hf_custom_token");
alert("Local token cleared.");
}
}
updateTokenWarningVisibility();
});
}
// Password visibility toggle
if (tokenToggleBtn) {
tokenToggleBtn.addEventListener("click", () => {
if (settingHfToken) {
if (settingHfToken.type === "password") {
settingHfToken.type = "text";
tokenToggleBtn.innerHTML = `Show`;
if (typeof lucide !== 'undefined') {
tokenToggleBtn.innerHTML = `<i data-lucide="eye-off" width="16" height="16"></i>`;
}
} else {
settingHfToken.type = "password";
tokenToggleBtn.innerHTML = `Hide`;
if (typeof lucide !== 'undefined') {
tokenToggleBtn.innerHTML = `<i data-lucide="eye" width="16" height="16"></i>`;
}
}
}
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
}
}
// Textarea height adjustment
function adjustTextareaHeight() {
if (!chatInput) return;
chatInput.style.height = "auto";
chatInput.style.height = (chatInput.scrollHeight) + "px";
}
function validateInput() {
if (!chatInput || !sendBtn) return;
const hasText = chatInput.value.trim().length > 0;
sendBtn.disabled = !hasText;
}
// Perform Message Send
async function sendMessage() {
if (!chatInput) return;
const text = chatInput.value.trim();
if (!text) return;
// Reset input field
chatInput.value = "";
adjustTextareaHeight();
if (sendBtn) sendBtn.disabled = true;
const session = sessions.find(s => s.id === activeSessionId);
if (!session) return;
// If empty, clean empty state text
if (!session.messages || session.messages.length === 0) {
if (messagesViewport) messagesViewport.innerHTML = "";
}
// If it's a new chat, auto-update title from the first message
if (!session.messages || session.messages.length === 0) {
session.messages = [];
session.title = text.length > 25 ? text.substring(0, 25) + "..." : text;
}
// Append User message
session.messages.push({ role: "user", content: text });
appendBubbleToViewport("user", text);
scrollToBottom();
renderSessions(); // update previews
// Append Bot message container & loading indicator
const botBubble = appendBubbleToViewport("assistant", "");
if (botBubble) {
botBubble.innerHTML = `
<div class="typing-indicator" id="loading-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
`;
}
scrollToBottom();
// Load API parameters
const systemPrompt = localStorage.getItem("model_system_prompt") || "You are a helpful assistant.";
const temp = parseFloat(localStorage.getItem("model_temperature") || "0.7");
const maxTokens = parseInt(localStorage.getItem("model_max_tokens") || "1024");
const customToken = localStorage.getItem("hf_custom_token") || "";
// Construct payload history (including system prompt)
const payloadMessages = [
{ role: "system", content: systemPrompt },
...session.messages.map(m => ({ role: m.role, content: m.content }))
];
let botResponse = "";
try {
if (!gradioClient) {
throw new Error("Gradio server client connection is not active yet. Please wait a moment and try again.");
}
// Submit request to Gradio Server Endpoint `/chat`
const submission = gradioClient.submit("/chat", {
messages_json: JSON.stringify(payloadMessages),
temperature: temp,
max_tokens: maxTokens,
custom_token: customToken
});
let isFirstToken = true;
// Stream response using Async Iterator
for await (const message of submission) {
if (message.type === "data" && message.data && message.data[0]) {
if (isFirstToken && botBubble) {
botBubble.innerHTML = "";
isFirstToken = false;
}
const chunk = message.data[0];
botResponse = chunk;
if (botBubble) {
botBubble.innerHTML = parseMarkdown(botResponse);
addCopyButtons(botBubble);
}
scrollToBottom();
}
}
// Complete interaction
if (botResponse) {
session.messages.push({ role: "assistant", content: botResponse });
saveSessionsToStorage();
renderSessions();
} else if (botBubble) {
botBubble.textContent = "No response received from the model.";
}
} catch (err) {
console.error("Chat streaming error:", err);
if (botBubble) {
botBubble.innerHTML = `<span style="color: #ef4444;">System Error: ${err.message}</span>`;
}
}
}
function scrollToBottom() {
if (messagesViewport) {
messagesViewport.scrollTop = messagesViewport.scrollHeight;
}
}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Initialize immediately if document is ready, otherwise listen to DOMContentLoaded
if (document.readyState === "complete" || document.readyState === "interactive") {
init();
} else {
window.addEventListener("DOMContentLoaded", init);
}
</script>
</body>
</html>