| <!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> |
| |
| |
| <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"> |
| |
| |
| <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> |
| |
| |
| <script src="https://unpkg.com/lucide@latest"></script> |
|
|
| |
| <style> |
| :root { |
| |
| --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; |
| } |
| |
| |
| [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; |
| } |
| |
| |
| [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; |
| } |
| |
| |
| .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 { |
| 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 { |
| 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 { |
| 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 { |
| 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-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); |
| } |
| |
| |
| .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-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-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); |
| } |
| |
| |
| .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 { |
| 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-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 { |
| 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; } |
| |
| |
| @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); } |
| } |
| |
| |
| @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> |
|
|
| |
| <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> |
|
|
| |
| <div class="sessions-list" id="sessions-list"> |
| |
| </div> |
|
|
| |
| <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 class="chat-container"> |
| |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="messages-viewport" id="messages-viewport"> |
| |
| </div> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <script type="module"> |
| import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| |
| 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```" |
| } |
| ]; |
| |
| |
| 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"); |
| |
| |
| 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"); |
| |
| |
| async function init() { |
| try { |
| |
| loadLocalSettings(); |
| |
| |
| loadSessionsFromStorage(); |
| |
| |
| setupEventListeners(); |
| |
| |
| renderSessions(); |
| if (activeSessionId) { |
| renderActiveChat(); |
| } else { |
| renderEmptyState(); |
| } |
| |
| |
| if (typeof lucide !== 'undefined') { |
| lucide.createIcons(); |
| } |
| } catch (e) { |
| console.error("Error during UI initialization:", e); |
| } |
| |
| |
| connectGradioClient(); |
| |
| |
| checkServerConfig(); |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| function updateTokenWarningVisibility() { |
| const hasLocalToken = !!localStorage.getItem("hf_custom_token"); |
| if (isConfigTokenAvailable || hasLocalToken) { |
| tokenWarningBanner.style.display = "none"; |
| } else { |
| tokenWarningBanner.style.display = "flex"; |
| } |
| } |
| |
| |
| function loadLocalSettings() { |
| |
| const savedTheme = localStorage.getItem("app_theme") || "default"; |
| if (themeSelect) { |
| themeSelect.value = savedTheme; |
| } |
| if (savedTheme !== "default") { |
| document.body.setAttribute("data-theme", savedTheme); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| const savedTemp = localStorage.getItem("model_temperature") || "0.7"; |
| if (settingTemp) { |
| settingTemp.value = savedTemp; |
| } |
| if (tempDisplay) { |
| tempDisplay.textContent = savedTemp; |
| } |
| |
| |
| const savedMaxTokens = localStorage.getItem("model_max_tokens") || "1024"; |
| if (settingMaxTokens) { |
| settingMaxTokens.value = savedMaxTokens; |
| } |
| if (maxTokensDisplay) { |
| maxTokensDisplay.textContent = savedMaxTokens; |
| } |
| |
| |
| const savedLocalToken = localStorage.getItem("hf_custom_token") || ""; |
| if (settingHfToken) { |
| settingHfToken.value = savedLocalToken; |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| 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> |
| `; |
| |
| |
| const delBtn = item.querySelector(".session-delete-btn"); |
| if (delBtn) { |
| delBtn.addEventListener("click", (e) => { |
| e.stopPropagation(); |
| deleteSession(sess.id); |
| }); |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| function renderEmptyState() { |
| if (!messagesViewport) return; |
| |
| |
| 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> |
| `; |
| |
| |
| 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(); |
| }); |
| }); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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>"); |
| } |
| } |
| |
| |
| 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); |
| |
| |
| const codeBlock = pre.querySelector("code"); |
| if (codeBlock && typeof hljs !== 'undefined') { |
| hljs.highlightElement(codeBlock); |
| } |
| }); |
| if (typeof lucide !== 'undefined') { |
| lucide.createIcons(); |
| } |
| } |
| |
| |
| function setupEventListeners() { |
| |
| if (chatInput) { |
| chatInput.addEventListener("input", () => { |
| adjustTextareaHeight(); |
| validateInput(); |
| }); |
| |
| |
| chatInput.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| if (sendBtn && !sendBtn.disabled) { |
| sendMessage(); |
| } |
| } |
| }); |
| } |
| |
| |
| if (sendBtn) { |
| sendBtn.addEventListener("click", sendMessage); |
| } |
| |
| |
| if (newChatBtn) { |
| newChatBtn.addEventListener("click", () => { |
| createNewSession(); |
| renderSessions(); |
| renderActiveChat(); |
| if (chatInput) { |
| chatInput.focus(); |
| } |
| }); |
| } |
| |
| |
| 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); |
| } |
| }); |
| } |
| |
| |
| 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(); |
| }); |
| } |
| |
| |
| 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); |
| }); |
| } |
| |
| |
| 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(); |
| }); |
| } |
| |
| |
| 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(); |
| } |
| }); |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| async function sendMessage() { |
| if (!chatInput) return; |
| const text = chatInput.value.trim(); |
| if (!text) return; |
| |
| |
| chatInput.value = ""; |
| adjustTextareaHeight(); |
| if (sendBtn) sendBtn.disabled = true; |
| |
| const session = sessions.find(s => s.id === activeSessionId); |
| if (!session) return; |
| |
| |
| if (!session.messages || session.messages.length === 0) { |
| if (messagesViewport) messagesViewport.innerHTML = ""; |
| } |
| |
| |
| if (!session.messages || session.messages.length === 0) { |
| session.messages = []; |
| session.title = text.length > 25 ? text.substring(0, 25) + "..." : text; |
| } |
| |
| |
| session.messages.push({ role: "user", content: text }); |
| appendBubbleToViewport("user", text); |
| scrollToBottom(); |
| renderSessions(); |
| |
| |
| 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(); |
| |
| |
| 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") || ""; |
| |
| |
| 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."); |
| } |
| |
| |
| const submission = gradioClient.submit("/chat", { |
| messages_json: JSON.stringify(payloadMessages), |
| temperature: temp, |
| max_tokens: maxTokens, |
| custom_token: customToken |
| }); |
| |
| let isFirstToken = true; |
| |
| |
| 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(); |
| } |
| } |
| |
| |
| 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, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">") |
| .replace(/"/g, """) |
| .replace(/'/g, "'"); |
| } |
| |
| |
| if (document.readyState === "complete" || document.readyState === "interactive") { |
| init(); |
| } else { |
| window.addEventListener("DOMContentLoaded", init); |
| } |
| </script> |
| </body> |
| </html> |
|
|
|
|