akhaliq HF Staff commited on
Commit
a16bf82
·
1 Parent(s): 073a6ea

feat: Add gradio.Server backend and premium custom frontend

Browse files
Files changed (3) hide show
  1. app.py +60 -0
  2. index.html +1589 -0
  3. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from fastapi.responses import HTMLResponse
4
+ from gradio import Server
5
+ from openai import AsyncOpenAI
6
+
7
+ app = Server()
8
+
9
+ @app.get("/", response_class=HTMLResponse)
10
+ async def homepage():
11
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
12
+ with open(html_path, "r", encoding="utf-8") as f:
13
+ return f.read()
14
+
15
+ @app.get("/config")
16
+ async def get_config():
17
+ token_exists = bool(os.environ.get("HF_TOKEN"))
18
+ return {"has_token": token_exists}
19
+
20
+ @app.api(name="chat")
21
+ async def chat(messages_json: str, temperature: float = 0.7, max_tokens: int = 1024, custom_token: str = None):
22
+ # Check for Hugging Face token: custom override or environment variable
23
+ hf_token = (custom_token and custom_token.strip()) or os.environ.get("HF_TOKEN")
24
+
25
+ if not hf_token:
26
+ yield "Error: Hugging Face Token (HF_TOKEN) is not configured. Please set it in your environment or provide it in the UI Settings panel."
27
+ return
28
+
29
+ try:
30
+ messages = json.loads(messages_json)
31
+ except Exception as e:
32
+ yield f"Error parsing chat messages: {str(e)}"
33
+ return
34
+
35
+ try:
36
+ client = AsyncOpenAI(
37
+ base_url="https://router.huggingface.co/v1",
38
+ api_key=hf_token,
39
+ default_headers={
40
+ "X-HF-Bill-To": "huggingface"
41
+ }
42
+ )
43
+
44
+ stream = await client.chat.completions.create(
45
+ model="nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4:together",
46
+ messages=messages,
47
+ temperature=temperature,
48
+ max_tokens=max_tokens,
49
+ stream=True,
50
+ )
51
+
52
+ async for chunk in stream:
53
+ if chunk.choices and chunk.choices[0].delta.content is not None:
54
+ yield chunk.choices[0].delta.content
55
+
56
+ except Exception as e:
57
+ yield f"Error calling Hugging Face Router: {str(e)}"
58
+
59
+ if __name__ == "__main__":
60
+ app.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,1589 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NVIDIA Nemotron 3 Ultra 550B - Chat Assistant</title>
7
+
8
+ <!-- Google Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <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">
12
+
13
+ <!-- Markdown Parser & Code Syntax Highlighter CDNs -->
14
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-dark.min.css">
16
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/core.min.js"></script>
17
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/python.min.js"></script>
18
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/javascript.min.js"></script>
19
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/bash.min.js"></script>
20
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/css.min.js"></script>
21
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/json.min.js"></script>
22
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/markdown.min.js"></script>
23
+
24
+ <!-- Lucide Icons -->
25
+ <script src="https://unpkg.com/lucide@latest"></script>
26
+
27
+ <!-- App Styling -->
28
+ <style>
29
+ :root {
30
+ /* Space Dark Theme (Default) */
31
+ --bg-gradient: radial-gradient(circle at 50% 50%, #0d0e15 0%, #050608 100%);
32
+ --sidebar-bg: rgba(13, 14, 21, 0.45);
33
+ --card-bg: rgba(20, 22, 33, 0.45);
34
+ --card-border: rgba(99, 102, 241, 0.15);
35
+ --card-border-hover: rgba(99, 102, 241, 0.35);
36
+ --text-main: #f3f4f6;
37
+ --text-muted: #9ca3af;
38
+ --primary: #6366f1;
39
+ --primary-glow: rgba(99, 102, 241, 0.35);
40
+ --secondary: #a855f7;
41
+ --accent: #10b981;
42
+ --user-bubble-bg: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);
43
+ --bot-bubble-bg: rgba(30, 32, 50, 0.5);
44
+ --input-bg: rgba(15, 17, 28, 0.6);
45
+ --glow-color: #6366f1;
46
+ --font-main: 'Outfit', sans-serif;
47
+ --font-mono: 'Fira Code', monospace;
48
+ --sidebar-width: 280px;
49
+ --settings-width: 320px;
50
+ }
51
+
52
+ /* Cyber Glow Theme */
53
+ [data-theme="cyber"] {
54
+ --bg-gradient: radial-gradient(circle at 50% 50%, #0a0c10 0%, #020304 100%);
55
+ --sidebar-bg: rgba(10, 12, 16, 0.6);
56
+ --card-bg: rgba(15, 18, 25, 0.5);
57
+ --card-border: rgba(6, 182, 212, 0.2);
58
+ --card-border-hover: rgba(236, 72, 153, 0.4);
59
+ --text-main: #e2e8f0;
60
+ --text-muted: #64748b;
61
+ --primary: #06b6d4;
62
+ --primary-glow: rgba(6, 182, 212, 0.4);
63
+ --secondary: #ec4899;
64
+ --accent: #f59e0b;
65
+ --user-bubble-bg: linear-gradient(135deg, #0891b2 0%, #0369a1 100%);
66
+ --bot-bubble-bg: rgba(21, 26, 38, 0.6);
67
+ --input-bg: rgba(11, 14, 20, 0.8);
68
+ --glow-color: #06b6d4;
69
+ }
70
+
71
+ /* Emerald Glass Theme */
72
+ [data-theme="emerald"] {
73
+ --bg-gradient: radial-gradient(circle at 50% 50%, #061512 0%, #020706 100%);
74
+ --sidebar-bg: rgba(6, 21, 18, 0.55);
75
+ --card-bg: rgba(10, 31, 27, 0.45);
76
+ --card-border: rgba(16, 185, 129, 0.15);
77
+ --card-border-hover: rgba(16, 185, 129, 0.35);
78
+ --text-main: #f0fdf4;
79
+ --text-muted: #86efac;
80
+ --primary: #10b981;
81
+ --primary-glow: rgba(16, 185, 129, 0.35);
82
+ --secondary: #14b8a6;
83
+ --accent: #fbbf24;
84
+ --user-bubble-bg: linear-gradient(135deg, #059669 0%, #047857 100%);
85
+ --bot-bubble-bg: rgba(15, 45, 39, 0.5);
86
+ --input-bg: rgba(7, 24, 20, 0.7);
87
+ --glow-color: #10b981;
88
+ }
89
+
90
+ * {
91
+ box-sizing: border-box;
92
+ margin: 0;
93
+ padding: 0;
94
+ scrollbar-width: thin;
95
+ scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
96
+ }
97
+
98
+ body {
99
+ font-family: var(--font-main);
100
+ background: var(--bg-gradient);
101
+ color: var(--text-main);
102
+ height: 100vh;
103
+ display: flex;
104
+ overflow: hidden;
105
+ letter-spacing: -0.01em;
106
+ }
107
+
108
+ /* Layout */
109
+ .sidebar {
110
+ width: var(--sidebar-width);
111
+ height: 100%;
112
+ background: var(--sidebar-bg);
113
+ backdrop-filter: blur(16px);
114
+ -webkit-backdrop-filter: blur(16px);
115
+ border-right: 1px solid var(--card-border);
116
+ display: flex;
117
+ flex-direction: column;
118
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
119
+ z-index: 10;
120
+ }
121
+
122
+ .chat-container {
123
+ flex: 1;
124
+ display: flex;
125
+ flex-direction: column;
126
+ height: 100%;
127
+ position: relative;
128
+ background: transparent;
129
+ }
130
+
131
+ /* Sidebar Header */
132
+ .sidebar-header {
133
+ padding: 20px;
134
+ border-bottom: 1px solid var(--card-border);
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ }
139
+
140
+ .logo-container {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 10px;
144
+ }
145
+
146
+ .logo-icon {
147
+ color: var(--primary);
148
+ filter: drop-shadow(0 0 8px var(--primary-glow));
149
+ }
150
+
151
+ .logo-text {
152
+ font-size: 1.1rem;
153
+ font-weight: 700;
154
+ background: linear-gradient(135deg, var(--text-main) 30%, var(--primary) 100%);
155
+ -webkit-background-clip: text;
156
+ -webkit-text-fill-color: transparent;
157
+ letter-spacing: -0.02em;
158
+ }
159
+
160
+ .new-chat-btn {
161
+ background: rgba(255, 255, 255, 0.05);
162
+ border: 1px solid var(--card-border);
163
+ color: var(--text-main);
164
+ padding: 8px 12px;
165
+ border-radius: 10px;
166
+ cursor: pointer;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ font-size: 0.85rem;
171
+ font-weight: 500;
172
+ transition: all 0.2s ease;
173
+ }
174
+
175
+ .new-chat-btn:hover {
176
+ background: var(--primary-glow);
177
+ border-color: var(--primary);
178
+ box-shadow: 0 0 12px var(--primary-glow);
179
+ }
180
+
181
+ /* Sessions List */
182
+ .sessions-list {
183
+ flex: 1;
184
+ overflow-y: auto;
185
+ padding: 15px 10px;
186
+ display: flex;
187
+ flex-direction: column;
188
+ gap: 8px;
189
+ }
190
+
191
+ .session-item {
192
+ padding: 12px;
193
+ border-radius: 12px;
194
+ border: 1px solid transparent;
195
+ cursor: pointer;
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ background: transparent;
200
+ transition: all 0.2s ease;
201
+ position: relative;
202
+ overflow: hidden;
203
+ }
204
+
205
+ .session-item:hover {
206
+ background: rgba(255, 255, 255, 0.03);
207
+ border-color: rgba(255, 255, 255, 0.05);
208
+ }
209
+
210
+ .session-item.active {
211
+ background: var(--card-bg);
212
+ border-color: var(--card-border);
213
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
214
+ }
215
+
216
+ .session-item.active::before {
217
+ content: '';
218
+ position: absolute;
219
+ left: 0;
220
+ top: 15%;
221
+ height: 70%;
222
+ width: 4px;
223
+ background: var(--primary);
224
+ border-radius: 0 4px 4px 0;
225
+ box-shadow: 0 0 8px var(--primary-glow);
226
+ }
227
+
228
+ .session-info {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 4px;
232
+ width: calc(100% - 30px);
233
+ }
234
+
235
+ .session-title {
236
+ font-size: 0.9rem;
237
+ font-weight: 500;
238
+ white-space: nowrap;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ }
242
+
243
+ .session-subtitle {
244
+ font-size: 0.75rem;
245
+ color: var(--text-muted);
246
+ white-space: nowrap;
247
+ overflow: hidden;
248
+ text-overflow: ellipsis;
249
+ }
250
+
251
+ .session-delete-btn {
252
+ background: transparent;
253
+ border: none;
254
+ color: var(--text-muted);
255
+ opacity: 0;
256
+ cursor: pointer;
257
+ padding: 4px;
258
+ border-radius: 6px;
259
+ transition: all 0.2s ease;
260
+ }
261
+
262
+ .session-item:hover .session-delete-btn {
263
+ opacity: 1;
264
+ }
265
+
266
+ .session-delete-btn:hover {
267
+ color: #ef4444;
268
+ background: rgba(239, 68, 68, 0.1);
269
+ }
270
+
271
+ /* Sidebar Footer */
272
+ .sidebar-footer {
273
+ padding: 15px;
274
+ border-top: 1px solid var(--card-border);
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: space-between;
278
+ }
279
+
280
+ .theme-select-container {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 8px;
284
+ }
285
+
286
+ .theme-select {
287
+ background: rgba(255, 255, 255, 0.05);
288
+ border: 1px solid var(--card-border);
289
+ color: var(--text-main);
290
+ padding: 6px 10px;
291
+ border-radius: 8px;
292
+ font-size: 0.8rem;
293
+ font-family: var(--font-main);
294
+ cursor: pointer;
295
+ outline: none;
296
+ }
297
+
298
+ /* Chat Header */
299
+ .chat-header {
300
+ height: 70px;
301
+ border-bottom: 1px solid var(--card-border);
302
+ backdrop-filter: blur(12px);
303
+ -webkit-backdrop-filter: blur(12px);
304
+ padding: 0 25px;
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: space-between;
308
+ z-index: 5;
309
+ }
310
+
311
+ .chat-title-container {
312
+ display: flex;
313
+ flex-direction: column;
314
+ }
315
+
316
+ .chat-title {
317
+ font-size: 1rem;
318
+ font-weight: 600;
319
+ }
320
+
321
+ .chat-status {
322
+ font-size: 0.75rem;
323
+ color: var(--accent);
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 5px;
327
+ }
328
+
329
+ .status-dot {
330
+ width: 6px;
331
+ height: 6px;
332
+ background-color: var(--accent);
333
+ border-radius: 50%;
334
+ box-shadow: 0 0 8px var(--accent);
335
+ }
336
+
337
+ .header-actions {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 12px;
341
+ }
342
+
343
+ .header-btn {
344
+ background: rgba(255, 255, 255, 0.04);
345
+ border: 1px solid var(--card-border);
346
+ color: var(--text-main);
347
+ width: 38px;
348
+ height: 38px;
349
+ border-radius: 10px;
350
+ cursor: pointer;
351
+ display: flex;
352
+ align-items: center;
353
+ justify-content: center;
354
+ transition: all 0.2s ease;
355
+ }
356
+
357
+ .header-btn:hover {
358
+ background: rgba(255, 255, 255, 0.08);
359
+ border-color: var(--primary);
360
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);
361
+ }
362
+
363
+ /* Messages Area */
364
+ .messages-viewport {
365
+ flex: 1;
366
+ overflow-y: auto;
367
+ padding: 30px 20px;
368
+ display: flex;
369
+ flex-direction: column;
370
+ gap: 25px;
371
+ }
372
+
373
+ .message-row {
374
+ display: flex;
375
+ width: 100%;
376
+ animation: fadeInUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
377
+ }
378
+
379
+ .message-row.user {
380
+ justify-content: flex-end;
381
+ }
382
+
383
+ .message-row.assistant {
384
+ justify-content: flex-start;
385
+ }
386
+
387
+ .message-bubble {
388
+ max-width: 75%;
389
+ padding: 16px 20px;
390
+ border-radius: 20px;
391
+ font-size: 0.95rem;
392
+ line-height: 1.6;
393
+ word-wrap: break-word;
394
+ position: relative;
395
+ }
396
+
397
+ .message-row.user .message-bubble {
398
+ background: var(--user-bubble-bg);
399
+ color: #ffffff;
400
+ border-bottom-right-radius: 4px;
401
+ box-shadow: 0 4px 15px rgba(79, 70, 229, 0.25);
402
+ }
403
+
404
+ .message-row.assistant .message-bubble {
405
+ background: var(--bot-bubble-bg);
406
+ color: var(--text-main);
407
+ border-bottom-left-radius: 4px;
408
+ border: 1px solid var(--card-border);
409
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
410
+ backdrop-filter: blur(8px);
411
+ }
412
+
413
+ /* Markdown Styles inside message bubbles */
414
+ .message-bubble p {
415
+ margin-bottom: 12px;
416
+ }
417
+ .message-bubble p:last-child {
418
+ margin-bottom: 0;
419
+ }
420
+ .message-bubble pre {
421
+ background: rgba(0, 0, 0, 0.3);
422
+ border-radius: 8px;
423
+ padding: 14px;
424
+ margin: 12px 0;
425
+ overflow-x: auto;
426
+ border: 1px solid rgba(255, 255, 255, 0.05);
427
+ position: relative;
428
+ }
429
+ .message-bubble code {
430
+ font-family: var(--font-mono);
431
+ font-size: 0.85rem;
432
+ background: rgba(255, 255, 255, 0.08);
433
+ padding: 2px 5px;
434
+ border-radius: 4px;
435
+ }
436
+ .message-bubble pre code {
437
+ background: transparent;
438
+ padding: 0;
439
+ border-radius: 0;
440
+ }
441
+
442
+ /* Copy Button for code blocks */
443
+ .copy-code-btn {
444
+ position: absolute;
445
+ top: 8px;
446
+ right: 8px;
447
+ background: rgba(255, 255, 255, 0.05);
448
+ border: 1px solid rgba(255, 255, 255, 0.1);
449
+ border-radius: 6px;
450
+ color: var(--text-muted);
451
+ padding: 4px 8px;
452
+ font-size: 0.75rem;
453
+ cursor: pointer;
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 4px;
457
+ opacity: 0;
458
+ transition: all 0.2s ease;
459
+ }
460
+ .message-bubble pre:hover .copy-code-btn {
461
+ opacity: 1;
462
+ }
463
+ .copy-code-btn:hover {
464
+ background: rgba(255, 255, 255, 0.15);
465
+ color: var(--text-main);
466
+ }
467
+
468
+ .message-bubble table {
469
+ width: 100%;
470
+ border-collapse: collapse;
471
+ margin: 15px 0;
472
+ font-size: 0.9rem;
473
+ }
474
+ .message-bubble th, .message-bubble td {
475
+ border: 1px solid var(--card-border);
476
+ padding: 8px 12px;
477
+ text-align: left;
478
+ }
479
+ .message-bubble th {
480
+ background: rgba(255, 255, 255, 0.05);
481
+ }
482
+
483
+ .message-bubble ul, .message-bubble ol {
484
+ margin-left: 20px;
485
+ margin-bottom: 12px;
486
+ }
487
+
488
+ /* Empty State */
489
+ .empty-chat-state {
490
+ display: flex;
491
+ flex-direction: column;
492
+ align-items: center;
493
+ justify-content: center;
494
+ height: 100%;
495
+ text-align: center;
496
+ padding: 40px;
497
+ gap: 20px;
498
+ animation: fadeIn 0.6s ease;
499
+ }
500
+
501
+ .empty-icon-box {
502
+ width: 80px;
503
+ height: 80px;
504
+ border-radius: 24px;
505
+ background: rgba(99, 102, 241, 0.06);
506
+ border: 1px solid var(--card-border);
507
+ display: flex;
508
+ align-items: center;
509
+ justify-content: center;
510
+ color: var(--primary);
511
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
512
+ position: relative;
513
+ }
514
+
515
+ .empty-icon-box::after {
516
+ content: '';
517
+ position: absolute;
518
+ width: 100%;
519
+ height: 100%;
520
+ border-radius: 24px;
521
+ box-shadow: 0 0 20px var(--primary-glow);
522
+ opacity: 0.5;
523
+ }
524
+
525
+ .empty-chat-state h2 {
526
+ font-size: 1.6rem;
527
+ font-weight: 700;
528
+ margin-bottom: 8px;
529
+ }
530
+
531
+ .empty-chat-state p {
532
+ color: var(--text-muted);
533
+ max-width: 480px;
534
+ font-size: 0.95rem;
535
+ line-height: 1.5;
536
+ }
537
+
538
+ .suggestions-grid {
539
+ display: grid;
540
+ grid-template-columns: repeat(2, 1fr);
541
+ gap: 12px;
542
+ max-width: 540px;
543
+ margin-top: 15px;
544
+ }
545
+
546
+ .suggestion-card {
547
+ background: var(--card-bg);
548
+ border: 1px solid var(--card-border);
549
+ padding: 15px;
550
+ border-radius: 14px;
551
+ cursor: pointer;
552
+ text-align: left;
553
+ transition: all 0.2s ease;
554
+ }
555
+
556
+ .suggestion-card:hover {
557
+ border-color: var(--primary);
558
+ background: rgba(255, 255, 255, 0.02);
559
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
560
+ transform: translateY(-2px);
561
+ }
562
+
563
+ .suggestion-card-title {
564
+ font-size: 0.85rem;
565
+ font-weight: 600;
566
+ color: var(--primary);
567
+ margin-bottom: 4px;
568
+ }
569
+
570
+ .suggestion-card-desc {
571
+ font-size: 0.8rem;
572
+ color: var(--text-muted);
573
+ }
574
+
575
+ /* Input Area */
576
+ .chat-input-area {
577
+ padding: 20px 25px 30px;
578
+ background: transparent;
579
+ z-index: 5;
580
+ }
581
+
582
+ .input-wrapper {
583
+ background: var(--input-bg);
584
+ border: 1px solid var(--card-border);
585
+ border-radius: 18px;
586
+ padding: 10px 14px;
587
+ display: flex;
588
+ align-items: center;
589
+ gap: 12px;
590
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
591
+ backdrop-filter: blur(12px);
592
+ -webkit-backdrop-filter: blur(12px);
593
+ transition: all 0.2s ease;
594
+ }
595
+
596
+ .input-wrapper:focus-within {
597
+ border-color: var(--primary);
598
+ box-shadow: 0 0 15px var(--primary-glow), 0 10px 30px rgba(0, 0, 0, 0.25);
599
+ }
600
+
601
+ .chat-input {
602
+ flex: 1;
603
+ background: transparent;
604
+ border: none;
605
+ outline: none;
606
+ color: var(--text-main);
607
+ font-family: var(--font-main);
608
+ font-size: 0.95rem;
609
+ resize: none;
610
+ max-height: 120px;
611
+ min-height: 24px;
612
+ padding: 6px 0;
613
+ }
614
+
615
+ .chat-input::placeholder {
616
+ color: var(--text-muted);
617
+ opacity: 0.7;
618
+ }
619
+
620
+ .send-btn {
621
+ background: var(--primary);
622
+ border: none;
623
+ color: #ffffff;
624
+ width: 36px;
625
+ height: 36px;
626
+ border-radius: 12px;
627
+ cursor: pointer;
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
632
+ box-shadow: 0 4px 10px var(--primary-glow);
633
+ }
634
+
635
+ .send-btn:hover {
636
+ box-shadow: 0 0 15px var(--glow-color);
637
+ transform: scale(1.05);
638
+ }
639
+
640
+ .send-btn:disabled {
641
+ background: rgba(255, 255, 255, 0.05);
642
+ color: var(--text-muted);
643
+ cursor: not-allowed;
644
+ box-shadow: none;
645
+ transform: none;
646
+ }
647
+
648
+ /* Settings Panel Drawer */
649
+ .settings-panel {
650
+ width: var(--settings-width);
651
+ height: 100%;
652
+ background: var(--sidebar-bg);
653
+ backdrop-filter: blur(16px);
654
+ -webkit-backdrop-filter: blur(16px);
655
+ border-left: 1px solid var(--card-border);
656
+ display: flex;
657
+ flex-direction: column;
658
+ position: absolute;
659
+ right: 0;
660
+ top: 0;
661
+ transform: translateX(100%);
662
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
663
+ z-index: 9;
664
+ box-shadow: -10px 0 30px rgba(0, 0, 0, 0.25);
665
+ }
666
+
667
+ .settings-panel.open {
668
+ transform: translateX(0);
669
+ }
670
+
671
+ .settings-header {
672
+ padding: 20px;
673
+ border-bottom: 1px solid var(--card-border);
674
+ display: flex;
675
+ align-items: center;
676
+ justify-content: space-between;
677
+ }
678
+
679
+ .settings-title {
680
+ font-size: 1rem;
681
+ font-weight: 600;
682
+ display: flex;
683
+ align-items: center;
684
+ gap: 8px;
685
+ }
686
+
687
+ .settings-body {
688
+ flex: 1;
689
+ overflow-y: auto;
690
+ padding: 20px;
691
+ display: flex;
692
+ flex-direction: column;
693
+ gap: 22px;
694
+ }
695
+
696
+ .setting-group {
697
+ display: flex;
698
+ flex-direction: column;
699
+ gap: 8px;
700
+ }
701
+
702
+ .setting-label {
703
+ font-size: 0.85rem;
704
+ font-weight: 600;
705
+ color: var(--text-main);
706
+ display: flex;
707
+ justify-content: space-between;
708
+ align-items: center;
709
+ }
710
+
711
+ .setting-value-display {
712
+ font-size: 0.8rem;
713
+ color: var(--primary);
714
+ font-family: var(--font-mono);
715
+ font-weight: 500;
716
+ }
717
+
718
+ .setting-input {
719
+ background: rgba(0, 0, 0, 0.25);
720
+ border: 1px solid var(--card-border);
721
+ color: var(--text-main);
722
+ padding: 10px 12px;
723
+ border-radius: 10px;
724
+ outline: none;
725
+ font-size: 0.9rem;
726
+ font-family: var(--font-main);
727
+ transition: all 0.2s ease;
728
+ }
729
+
730
+ .setting-input:focus {
731
+ border-color: var(--primary);
732
+ box-shadow: 0 0 8px var(--primary-glow);
733
+ }
734
+
735
+ .setting-textarea {
736
+ min-height: 80px;
737
+ max-height: 180px;
738
+ resize: vertical;
739
+ }
740
+
741
+ .slider-wrapper {
742
+ display: flex;
743
+ align-items: center;
744
+ gap: 10px;
745
+ }
746
+
747
+ .setting-slider {
748
+ flex: 1;
749
+ height: 5px;
750
+ background: rgba(255, 255, 255, 0.1);
751
+ border-radius: 5px;
752
+ outline: none;
753
+ -webkit-appearance: none;
754
+ accent-color: var(--primary);
755
+ }
756
+
757
+ .token-input-container {
758
+ display: flex;
759
+ position: relative;
760
+ }
761
+
762
+ .token-input-container input {
763
+ width: 100%;
764
+ padding-right: 38px;
765
+ }
766
+
767
+ .token-toggle-btn {
768
+ position: absolute;
769
+ right: 10px;
770
+ top: 50%;
771
+ transform: translateY(-50%);
772
+ background: transparent;
773
+ border: none;
774
+ color: var(--text-muted);
775
+ cursor: pointer;
776
+ padding: 4px;
777
+ display: flex;
778
+ align-items: center;
779
+ }
780
+
781
+ .token-save-btn {
782
+ background: var(--primary);
783
+ border: none;
784
+ color: #ffffff;
785
+ padding: 10px;
786
+ border-radius: 10px;
787
+ cursor: pointer;
788
+ font-weight: 600;
789
+ font-size: 0.85rem;
790
+ transition: all 0.2s ease;
791
+ text-align: center;
792
+ box-shadow: 0 4px 10px var(--primary-glow);
793
+ }
794
+
795
+ .token-save-btn:hover {
796
+ box-shadow: 0 0 15px var(--glow-color);
797
+ }
798
+
799
+ /* Token missing Banner */
800
+ .token-warning-banner {
801
+ background: rgba(245, 158, 11, 0.15);
802
+ border: 1px solid rgba(245, 158, 11, 0.3);
803
+ border-radius: 12px;
804
+ padding: 12px 16px;
805
+ display: flex;
806
+ align-items: flex-start;
807
+ gap: 12px;
808
+ margin: 20px 20px 0;
809
+ animation: fadeIn 0.4s ease;
810
+ }
811
+
812
+ .token-warning-banner-text {
813
+ font-size: 0.85rem;
814
+ color: #fbd38d;
815
+ line-height: 1.4;
816
+ }
817
+
818
+ .token-warning-banner-link {
819
+ color: #f6ad55;
820
+ text-decoration: underline;
821
+ cursor: pointer;
822
+ font-weight: 600;
823
+ }
824
+
825
+ /* Typing indicator */
826
+ .typing-indicator {
827
+ display: flex;
828
+ gap: 6px;
829
+ align-items: center;
830
+ height: 20px;
831
+ padding-left: 4px;
832
+ }
833
+
834
+ .typing-dot {
835
+ width: 7px;
836
+ height: 7px;
837
+ background-color: var(--text-muted);
838
+ border-radius: 50%;
839
+ animation: bounce 1.4s infinite ease-in-out both;
840
+ }
841
+
842
+ .typing-dot:nth-child(1) { animation-delay: -0.32s; }
843
+ .typing-dot:nth-child(2) { animation-delay: -0.16s; }
844
+
845
+ /* Animations */
846
+ @keyframes fadeInUp {
847
+ from {
848
+ opacity: 0;
849
+ transform: translateY(12px);
850
+ }
851
+ to {
852
+ opacity: 1;
853
+ transform: translateY(0);
854
+ }
855
+ }
856
+
857
+ @keyframes fadeIn {
858
+ from { opacity: 0; }
859
+ to { opacity: 1; }
860
+ }
861
+
862
+ @keyframes bounce {
863
+ 0%, 80%, 100% { transform: scale(0); }
864
+ 40% { transform: scale(1.0); }
865
+ }
866
+
867
+ /* Responsive */
868
+ @media (max-width: 768px) {
869
+ .sidebar {
870
+ position: absolute;
871
+ left: -100%;
872
+ top: 0;
873
+ height: 100%;
874
+ z-index: 10;
875
+ }
876
+ .sidebar.mobile-open {
877
+ left: 0;
878
+ }
879
+ .message-bubble {
880
+ max-width: 85%;
881
+ }
882
+ }
883
+ </style>
884
+ </head>
885
+ <body>
886
+
887
+ <!-- Sidebar: Sessions History -->
888
+ <aside class="sidebar" id="sidebar">
889
+ <div class="sidebar-header">
890
+ <div class="logo-container">
891
+ <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>
892
+ <span class="logo-text">Nemotron 3</span>
893
+ </div>
894
+ <button class="new-chat-btn" id="new-chat-btn">
895
+ <i data-lucide="plus" width="14" height="14"></i>
896
+ New Chat
897
+ </button>
898
+ </div>
899
+
900
+ <!-- Sessions List -->
901
+ <div class="sessions-list" id="sessions-list">
902
+ <!-- Dynamic elements loaded via JS -->
903
+ </div>
904
+
905
+ <!-- Sidebar Footer & Theme Toggle -->
906
+ <div class="sidebar-footer">
907
+ <div class="theme-select-container">
908
+ <i data-lucide="palette" width="16" height="16" style="color: var(--primary);"></i>
909
+ <select class="theme-select" id="theme-select">
910
+ <option value="default">Space Dark</option>
911
+ <option value="cyber">Cyber Glow</option>
912
+ <option value="emerald">Emerald Glass</option>
913
+ </select>
914
+ </div>
915
+ </div>
916
+ </aside>
917
+
918
+ <!-- Main Chat Workspace -->
919
+ <main class="chat-container">
920
+
921
+ <!-- Header -->
922
+ <header class="chat-header">
923
+ <div class="chat-title-container">
924
+ <div class="chat-title">Nemotron-3-Ultra-550B</div>
925
+ <div class="chat-status">
926
+ <span class="status-dot"></span>
927
+ <span>Ready</span>
928
+ </div>
929
+ </div>
930
+ <div class="header-actions">
931
+ <button class="header-btn" id="sidebar-toggle-btn" style="display: none;">
932
+ <i data-lucide="menu" width="18" height="18"></i>
933
+ </button>
934
+ <button class="header-btn" id="settings-toggle-btn">
935
+ <i data-lucide="settings" width="18" height="18"></i>
936
+ </button>
937
+ </div>
938
+ </header>
939
+
940
+ <!-- Warning banner if token is missing -->
941
+ <div class="token-warning-banner" id="token-warning-banner" style="display: none;">
942
+ <i data-lucide="key-round" width="18" height="18" style="color: #fbbf24; flex-shrink: 0;"></i>
943
+ <div class="token-warning-banner-text">
944
+ Hugging Face Token is not set. The assistant will not be able to answer.
945
+ Please enter a token in the <span class="token-warning-banner-link" id="token-banner-link">Settings panel</span>.
946
+ </div>
947
+ </div>
948
+
949
+ <!-- Message Viewport -->
950
+ <div class="messages-viewport" id="messages-viewport">
951
+ <!-- Empty state or chat bubbles loaded via JS -->
952
+ </div>
953
+
954
+ <!-- Message Input Area -->
955
+ <div class="chat-input-area">
956
+ <div class="input-wrapper">
957
+ <textarea class="chat-input" id="chat-input" rows="1" placeholder="Type a message..."></textarea>
958
+ <button class="send-btn" id="send-btn" disabled>
959
+ <i data-lucide="arrow-up" width="18" height="18"></i>
960
+ </button>
961
+ </div>
962
+ </div>
963
+
964
+ <!-- Settings drawer panel -->
965
+ <div class="settings-panel" id="settings-panel">
966
+ <div class="settings-header">
967
+ <div class="settings-title">
968
+ <i data-lucide="sliders" width="18" height="18" style="color: var(--primary);"></i>
969
+ Model Parameters
970
+ </div>
971
+ <button class="header-btn" id="settings-close-btn">
972
+ <i data-lucide="x" width="18" height="18"></i>
973
+ </button>
974
+ </div>
975
+ <div class="settings-body">
976
+ <!-- System Prompt -->
977
+ <div class="setting-group">
978
+ <label class="setting-label" for="setting-system-prompt">System Prompt</label>
979
+ <textarea class="setting-input setting-textarea" id="setting-system-prompt" placeholder="Custom instructions for the model..."></textarea>
980
+ </div>
981
+
982
+ <!-- Temperature Slider -->
983
+ <div class="setting-group">
984
+ <div class="setting-label">
985
+ <span>Temperature</span>
986
+ <span class="setting-value-display" id="temp-display">0.7</span>
987
+ </div>
988
+ <div class="slider-wrapper">
989
+ <input type="range" class="setting-slider" id="setting-temp" min="0" max="1.5" step="0.1" value="0.7">
990
+ </div>
991
+ </div>
992
+
993
+ <!-- Max Tokens Slider -->
994
+ <div class="setting-group">
995
+ <div class="setting-label">
996
+ <span>Max Tokens</span>
997
+ <span class="setting-value-display" id="max-tokens-display">1024</span>
998
+ </div>
999
+ <div class="slider-wrapper">
1000
+ <input type="range" class="setting-slider" id="setting-max-tokens" min="64" max="4096" step="64" value="1024">
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <!-- Custom Hugging Face Token -->
1005
+ <div class="setting-group" style="border-top: 1px solid var(--card-border); padding-top: 15px; margin-top: 10px;">
1006
+ <label class="setting-label">HF Token (Optional Override)</label>
1007
+ <div class="token-input-container">
1008
+ <input type="password" class="setting-input" id="setting-hf-token" placeholder="hf_...">
1009
+ <button class="token-toggle-btn" id="token-toggle-btn" type="button">
1010
+ <i data-lucide="eye" width="16" height="16"></i>
1011
+ </button>
1012
+ </div>
1013
+ <button class="token-save-btn" id="token-save-btn">Save Token Locally</button>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ </main>
1018
+
1019
+ <!-- Gradio Client and Main App Script -->
1020
+ <script type="module">
1021
+ import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
1022
+
1023
+ // Global State variables
1024
+ let sessions = [];
1025
+ let activeSessionId = null;
1026
+ let isConfigTokenAvailable = false;
1027
+ let gradioClient = null;
1028
+
1029
+ // Dom elements
1030
+ const chatInput = document.getElementById("chat-input");
1031
+ const sendBtn = document.getElementById("send-btn");
1032
+ const messagesViewport = document.getElementById("messages-viewport");
1033
+ const sessionsList = document.getElementById("sessions-list");
1034
+ const newChatBtn = document.getElementById("new-chat-btn");
1035
+ const themeSelect = document.getElementById("theme-select");
1036
+ const settingsToggleBtn = document.getElementById("settings-toggle-btn");
1037
+ const settingsCloseBtn = document.getElementById("settings-close-btn");
1038
+ const settingsPanel = document.getElementById("settings-panel");
1039
+ const tokenWarningBanner = document.getElementById("token-warning-banner");
1040
+ const tokenBannerLink = document.getElementById("token-banner-link");
1041
+
1042
+ // Parameter elements
1043
+ const settingSystemPrompt = document.getElementById("setting-system-prompt");
1044
+ const settingTemp = document.getElementById("setting-temp");
1045
+ const tempDisplay = document.getElementById("temp-display");
1046
+ const settingMaxTokens = document.getElementById("setting-max-tokens");
1047
+ const maxTokensDisplay = document.getElementById("max-tokens-display");
1048
+ const settingHfToken = document.getElementById("setting-hf-token");
1049
+ const tokenToggleBtn = document.getElementById("token-toggle-btn");
1050
+ const tokenSaveBtn = document.getElementById("token-save-btn");
1051
+
1052
+ // Initialization
1053
+ async function init() {
1054
+ // Load saved settings
1055
+ loadLocalSettings();
1056
+
1057
+ // Connect to Gradio Backend
1058
+ try {
1059
+ gradioClient = await Client.connect(window.location.origin);
1060
+ } catch (err) {
1061
+ console.error("Failed to connect to Gradio backend:", err);
1062
+ }
1063
+
1064
+ // Check if server environment has HF_TOKEN
1065
+ await checkServerConfig();
1066
+
1067
+ // Load sessions
1068
+ loadSessionsFromStorage();
1069
+
1070
+ // Render sessions
1071
+ renderSessions();
1072
+
1073
+ // Render active chat
1074
+ if (activeSessionId) {
1075
+ renderActiveChat();
1076
+ } else {
1077
+ renderEmptyState();
1078
+ }
1079
+
1080
+ setupEventListeners();
1081
+ lucide.createIcons();
1082
+ }
1083
+
1084
+ // Check if HF_TOKEN is in server env
1085
+ async function checkServerConfig() {
1086
+ try {
1087
+ const res = await fetch("/config");
1088
+ const config = await res.json();
1089
+ isConfigTokenAvailable = config.has_token;
1090
+ } catch (e) {
1091
+ console.error("Error reading config endpoint:", e);
1092
+ isConfigTokenAvailable = false;
1093
+ }
1094
+ updateTokenWarningVisibility();
1095
+ }
1096
+
1097
+ // Show/Hide token warning banner based on config availability or local storage override
1098
+ function updateTokenWarningVisibility() {
1099
+ const hasLocalToken = !!localStorage.getItem("hf_custom_token");
1100
+ if (isConfigTokenAvailable || hasLocalToken) {
1101
+ tokenWarningBanner.style.display = "none";
1102
+ } else {
1103
+ tokenWarningBanner.style.display = "flex";
1104
+ }
1105
+ }
1106
+
1107
+ // Local Storage settings loading
1108
+ function loadLocalSettings() {
1109
+ // Theme selection
1110
+ const savedTheme = localStorage.getItem("app_theme") || "default";
1111
+ themeSelect.value = savedTheme;
1112
+ if (savedTheme !== "default") {
1113
+ document.body.setAttribute("data-theme", savedTheme);
1114
+ }
1115
+
1116
+ // System prompt
1117
+ const savedSystemPrompt = localStorage.getItem("model_system_prompt") ||
1118
+ "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.";
1119
+ settingSystemPrompt.value = savedSystemPrompt;
1120
+
1121
+ // Temperature
1122
+ const savedTemp = localStorage.getItem("model_temperature") || "0.7";
1123
+ settingTemp.value = savedTemp;
1124
+ tempDisplay.textContent = savedTemp;
1125
+
1126
+ // Max Tokens
1127
+ const savedMaxTokens = localStorage.getItem("model_max_tokens") || "1024";
1128
+ settingMaxTokens.value = savedMaxTokens;
1129
+ maxTokensDisplay.textContent = savedMaxTokens;
1130
+
1131
+ // Local Token
1132
+ const savedLocalToken = localStorage.getItem("hf_custom_token") || "";
1133
+ settingHfToken.value = savedLocalToken;
1134
+ }
1135
+
1136
+ // Load sessions list from localStorage
1137
+ function loadSessionsFromStorage() {
1138
+ const rawSessions = localStorage.getItem("chat_sessions");
1139
+ if (rawSessions) {
1140
+ try {
1141
+ sessions = json_parse_safe(rawSessions, []);
1142
+ const rawActiveId = localStorage.getItem("active_session_id");
1143
+ if (rawActiveId && sessions.some(s => s.id === rawActiveId)) {
1144
+ activeSessionId = rawActiveId;
1145
+ } else if (sessions.length > 0) {
1146
+ activeSessionId = sessions[0].id;
1147
+ }
1148
+ } catch (e) {
1149
+ sessions = [];
1150
+ }
1151
+ }
1152
+
1153
+ // Create a default session if list is empty
1154
+ if (sessions.length === 0) {
1155
+ createNewSession();
1156
+ }
1157
+ }
1158
+
1159
+ function saveSessionsToStorage() {
1160
+ localStorage.setItem("chat_sessions", JSON.stringify(sessions));
1161
+ if (activeSessionId) {
1162
+ localStorage.setItem("active_session_id", activeSessionId);
1163
+ } else {
1164
+ localStorage.removeItem("active_session_id");
1165
+ }
1166
+ }
1167
+
1168
+ function createNewSession() {
1169
+ const newSession = {
1170
+ id: 'sess_' + Date.now(),
1171
+ title: 'New Chat session',
1172
+ messages: [],
1173
+ timestamp: new Date().toLocaleString()
1174
+ };
1175
+ sessions.unshift(newSession);
1176
+ activeSessionId = newSession.id;
1177
+ saveSessionsToStorage();
1178
+ }
1179
+
1180
+ function json_parse_safe(str, fallback) {
1181
+ try { return JSON.parse(str); } catch (e) { return fallback; }
1182
+ }
1183
+
1184
+ // Render Sidebar Sessions List
1185
+ function renderSessions() {
1186
+ sessionsList.innerHTML = "";
1187
+ sessions.forEach(sess => {
1188
+ const item = document.createElement("div");
1189
+ item.className = `session-item ${sess.id === activeSessionId ? 'active' : ''}`;
1190
+ item.dataset.id = sess.id;
1191
+
1192
+ const lastMsg = sess.messages.length > 0 ? sess.messages[sess.messages.length - 1].content : "No messages yet";
1193
+
1194
+ item.innerHTML = `
1195
+ <div class="session-info">
1196
+ <div class="session-title">${escapeHtml(sess.title)}</div>
1197
+ <div class="session-subtitle">${escapeHtml(lastMsg)}</div>
1198
+ </div>
1199
+ <button class="session-delete-btn" title="Delete conversation">
1200
+ <i data-lucide="trash-2" width="14" height="14"></i>
1201
+ </button>
1202
+ `;
1203
+
1204
+ // Add delete handler
1205
+ item.querySelector(".session-delete-btn").addEventListener("click", (e) => {
1206
+ e.stopPropagation();
1207
+ deleteSession(sess.id);
1208
+ });
1209
+
1210
+ // Select handler
1211
+ item.addEventListener("click", () => {
1212
+ selectSession(sess.id);
1213
+ });
1214
+
1215
+ sessionsList.appendChild(item);
1216
+ });
1217
+ lucide.createIcons();
1218
+ }
1219
+
1220
+ function selectSession(id) {
1221
+ activeSessionId = id;
1222
+ saveSessionsToStorage();
1223
+ renderSessions();
1224
+ renderActiveChat();
1225
+ }
1226
+
1227
+ function deleteSession(id) {
1228
+ sessions = sessions.filter(s => s.id !== id);
1229
+ if (activeSessionId === id) {
1230
+ activeSessionId = sessions.length > 0 ? sessions[0].id : null;
1231
+ }
1232
+ if (sessions.length === 0) {
1233
+ createNewSession();
1234
+ }
1235
+ saveSessionsToStorage();
1236
+ renderSessions();
1237
+ renderActiveChat();
1238
+ }
1239
+
1240
+ // Render Active Chat Bubbles
1241
+ function renderActiveChat() {
1242
+ const currentSession = sessions.find(s => s.id === activeSessionId);
1243
+ if (!currentSession || currentSession.messages.length === 0) {
1244
+ renderEmptyState();
1245
+ return;
1246
+ }
1247
+
1248
+ messagesViewport.innerHTML = "";
1249
+ currentSession.messages.forEach(msg => {
1250
+ appendBubbleToViewport(msg.role, msg.content);
1251
+ });
1252
+ scrollToBottom();
1253
+ }
1254
+
1255
+ // Render Empty state when there are no messages
1256
+ function renderEmptyState() {
1257
+ messagesViewport.innerHTML = `
1258
+ <div class="empty-chat-state">
1259
+ <div class="empty-icon-box">
1260
+ <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>
1261
+ </div>
1262
+ <div>
1263
+ <h2>NVIDIA Nemotron 3 Ultra Chat</h2>
1264
+ <p>Ask anything! This model is highly tuned for complex instructions, coding help, and deep reasoning. Powered by NVIDIA's 550B Ultra architecture.</p>
1265
+ </div>
1266
+ <div class="suggestions-grid">
1267
+ <div class="suggestion-card" data-prompt="Explain the difference between supervised fine-tuning (SFT) and reinforcement learning from human feedback (RLHF).">
1268
+ <div class="suggestion-card-title">Explain AI Concepts</div>
1269
+ <div class="suggestion-card-desc">"SFT vs RLHF..."</div>
1270
+ </div>
1271
+ <div class="suggestion-card" data-prompt="Write a fast Python script to parallelize image loading using threads.">
1272
+ <div class="suggestion-card-title">Code Generation</div>
1273
+ <div class="suggestion-card-desc">"Parallel image loading in Python..."</div>
1274
+ </div>
1275
+ <div class="suggestion-card" data-prompt="Outline a comprehensive marketing strategy to launch a new SaaS developer tool.">
1276
+ <div class="suggestion-card-title">Design a Plan</div>
1277
+ <div class="suggestion-card-desc">"Marketing strategy for a SaaS tool..."</div>
1278
+ </div>
1279
+ <div class="suggestion-card" data-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```">
1280
+ <div class="suggestion-card-title">Code Review</div>
1281
+ <div class="suggestion-card-desc">"Find optimization bottlenecks..."</div>
1282
+ </div>
1283
+ </div>
1284
+ </div>
1285
+ `;
1286
+
1287
+ // Suggestion click listeners
1288
+ document.querySelectorAll(".suggestion-card").forEach(card => {
1289
+ card.addEventListener("click", () => {
1290
+ const prompt = card.dataset.prompt;
1291
+ chatInput.value = prompt;
1292
+ adjustTextareaHeight();
1293
+ chatInput.focus();
1294
+ validateInput();
1295
+ });
1296
+ });
1297
+ }
1298
+
1299
+ // Appending standard bubbles (using Markdown rendering for assistant)
1300
+ function appendBubbleToViewport(role, content) {
1301
+ const row = document.createElement("div");
1302
+ row.className = `message-row ${role}`;
1303
+
1304
+ const bubble = document.createElement("div");
1305
+ bubble.className = "message-bubble";
1306
+
1307
+ if (role === "user") {
1308
+ bubble.textContent = content;
1309
+ } else {
1310
+ // Assist message uses Marked parser
1311
+ bubble.innerHTML = parseMarkdown(content);
1312
+ addCopyButtons(bubble);
1313
+ }
1314
+
1315
+ row.appendChild(bubble);
1316
+ messagesViewport.appendChild(row);
1317
+ return bubble;
1318
+ }
1319
+
1320
+ // Parse Markdown safely
1321
+ function parseMarkdown(text) {
1322
+ try {
1323
+ return marked.parse(text);
1324
+ } catch (e) {
1325
+ return escapeHtml(text);
1326
+ }
1327
+ }
1328
+
1329
+ // Add copy code triggers to <pre> blocks
1330
+ function addCopyButtons(container) {
1331
+ container.querySelectorAll("pre").forEach(pre => {
1332
+ // Check if already has button
1333
+ if (pre.querySelector(".copy-code-btn")) return;
1334
+
1335
+ const btn = document.createElement("button");
1336
+ btn.className = "copy-code-btn";
1337
+ btn.innerHTML = `<i data-lucide="copy" width="12" height="12"></i> Copy`;
1338
+
1339
+ btn.addEventListener("click", () => {
1340
+ const code = pre.querySelector("code")?.textContent || pre.textContent;
1341
+ navigator.clipboard.writeText(code).then(() => {
1342
+ btn.innerHTML = `<i data-lucide="check" width="12" height="12" style="color: var(--accent);"></i> Copied!`;
1343
+ setTimeout(() => {
1344
+ btn.innerHTML = `<i data-lucide="copy" width="12" height="12"></i> Copy`;
1345
+ lucide.createIcons();
1346
+ }, 2000);
1347
+ lucide.createIcons();
1348
+ });
1349
+ });
1350
+
1351
+ pre.appendChild(btn);
1352
+
1353
+ // Highlight block
1354
+ const codeBlock = pre.querySelector("code");
1355
+ if (codeBlock) {
1356
+ hljs.highlightElement(codeBlock);
1357
+ }
1358
+ });
1359
+ lucide.createIcons();
1360
+ }
1361
+
1362
+ // Event listener setup
1363
+ function setupEventListeners() {
1364
+ // Typing input auto height & validation
1365
+ chatInput.addEventListener("input", () => {
1366
+ adjustTextareaHeight();
1367
+ validateInput();
1368
+ });
1369
+
1370
+ // Enter key to submit (Shift+Enter for newline)
1371
+ chatInput.addEventListener("keydown", (e) => {
1372
+ if (e.key === "Enter" && !e.shiftKey) {
1373
+ e.preventDefault();
1374
+ if (!sendBtn.disabled) {
1375
+ sendMessage();
1376
+ }
1377
+ }
1378
+ });
1379
+
1380
+ // Send trigger
1381
+ sendBtn.addEventListener("click", sendMessage);
1382
+
1383
+ // New session
1384
+ newChatBtn.addEventListener("click", () => {
1385
+ createNewSession();
1386
+ renderSessions();
1387
+ renderActiveChat();
1388
+ chatInput.focus();
1389
+ });
1390
+
1391
+ // Theme selector change
1392
+ themeSelect.addEventListener("change", (e) => {
1393
+ const val = e.target.value;
1394
+ localStorage.setItem("app_theme", val);
1395
+ if (val === "default") {
1396
+ document.body.removeAttribute("data-theme");
1397
+ } else {
1398
+ document.body.setAttribute("data-theme", val);
1399
+ }
1400
+ });
1401
+
1402
+ // Settings Toggle
1403
+ settingsToggleBtn.addEventListener("click", () => {
1404
+ settingsPanel.classList.toggle("open");
1405
+ });
1406
+
1407
+ settingsCloseBtn.addEventListener("click", () => {
1408
+ settingsPanel.classList.remove("open");
1409
+ });
1410
+
1411
+ tokenBannerLink.addEventListener("click", () => {
1412
+ settingsPanel.classList.add("open");
1413
+ settingHfToken.focus();
1414
+ });
1415
+
1416
+ // Parameter updates listeners
1417
+ settingTemp.addEventListener("input", (e) => {
1418
+ const val = e.target.value;
1419
+ tempDisplay.textContent = val;
1420
+ localStorage.setItem("model_temperature", val);
1421
+ });
1422
+
1423
+ settingMaxTokens.addEventListener("input", (e) => {
1424
+ const val = e.target.value;
1425
+ maxTokensDisplay.textContent = val;
1426
+ localStorage.setItem("model_max_tokens", val);
1427
+ });
1428
+
1429
+ settingSystemPrompt.addEventListener("change", (e) => {
1430
+ localStorage.setItem("model_system_prompt", e.target.value);
1431
+ });
1432
+
1433
+ // Save Token trigger
1434
+ tokenSaveBtn.addEventListener("click", () => {
1435
+ const tok = settingHfToken.value.trim();
1436
+ if (tok) {
1437
+ localStorage.setItem("hf_custom_token", tok);
1438
+ alert("Token saved locally in browser storage!");
1439
+ } else {
1440
+ localStorage.removeItem("hf_custom_token");
1441
+ alert("Local token cleared.");
1442
+ }
1443
+ updateTokenWarningVisibility();
1444
+ });
1445
+
1446
+ // Password visibility toggle
1447
+ tokenToggleBtn.addEventListener("click", () => {
1448
+ if (settingHfToken.type === "password") {
1449
+ settingHfToken.type = "text";
1450
+ tokenToggleBtn.innerHTML = `<i data-lucide="eye-off" width="16" height="16"></i>`;
1451
+ } else {
1452
+ settingHfToken.type = "password";
1453
+ tokenToggleBtn.innerHTML = `<i data-lucide="eye" width="16" height="16"></i>`;
1454
+ }
1455
+ lucide.createIcons();
1456
+ });
1457
+ }
1458
+
1459
+ // Textarea height adjustment
1460
+ function adjustTextareaHeight() {
1461
+ chatInput.style.height = "auto";
1462
+ chatInput.style.height = (chatInput.scrollHeight) + "px";
1463
+ }
1464
+
1465
+ function validateInput() {
1466
+ const hasText = chatInput.value.trim().length > 0;
1467
+ sendBtn.disabled = !hasText;
1468
+ }
1469
+
1470
+ // Perform Message Send
1471
+ async function sendMessage() {
1472
+ const text = chatInput.value.trim();
1473
+ if (!text) return;
1474
+
1475
+ // Reset input field
1476
+ chatInput.value = "";
1477
+ adjustTextareaHeight();
1478
+ sendBtn.disabled = true;
1479
+
1480
+ const session = sessions.find(s => s.id === activeSessionId);
1481
+ if (!session) return;
1482
+
1483
+ // If empty, clean empty state text
1484
+ if (session.messages.length === 0) {
1485
+ messagesViewport.innerHTML = "";
1486
+ }
1487
+
1488
+ // If it's a new chat, auto-update title from the first message
1489
+ if (session.messages.length === 0) {
1490
+ session.title = text.length > 25 ? text.substring(0, 25) + "..." : text;
1491
+ }
1492
+
1493
+ // Append User message
1494
+ session.messages.push({ role: "user", content: text });
1495
+ appendBubbleToViewport("user", text);
1496
+ scrollToBottom();
1497
+ renderSessions(); // update previews
1498
+
1499
+ // Append Bot message container & loading indicator
1500
+ const botBubble = appendBubbleToViewport("assistant", "");
1501
+ botBubble.innerHTML = `
1502
+ <div class="typing-indicator" id="loading-indicator">
1503
+ <div class="typing-dot"></div>
1504
+ <div class="typing-dot"></div>
1505
+ <div class="typing-dot"></div>
1506
+ </div>
1507
+ `;
1508
+ scrollToBottom();
1509
+
1510
+ // Load API parameters
1511
+ const systemPrompt = localStorage.getItem("model_system_prompt") || "You are a helpful assistant.";
1512
+ const temp = parseFloat(localStorage.getItem("model_temperature") || "0.7");
1513
+ const maxTokens = parseInt(localStorage.getItem("model_max_tokens") || "1024");
1514
+ const customToken = localStorage.getItem("hf_custom_token") || "";
1515
+
1516
+ // Construct payload history (including system prompt)
1517
+ const payloadMessages = [
1518
+ { role: "system", content: systemPrompt },
1519
+ ...session.messages.map(m => ({ role: m.role, content: m.content }))
1520
+ ];
1521
+
1522
+ let botResponse = "";
1523
+
1524
+ try {
1525
+ if (!gradioClient) {
1526
+ throw new Error("Gradio server client connection is not active.");
1527
+ }
1528
+
1529
+ // Submit request to Gradio Server Endpoint `/chat`
1530
+ const submission = gradioClient.submit("/chat", {
1531
+ messages_json: JSON.stringify(payloadMessages),
1532
+ temperature: temp,
1533
+ max_tokens: maxTokens,
1534
+ custom_token: customToken
1535
+ });
1536
+
1537
+ let isFirstToken = true;
1538
+
1539
+ // Stream response using Async Iterator
1540
+ for await (const message of submission) {
1541
+ if (message.type === "data" && message.data && message.data[0]) {
1542
+ // Remove typing indicator on first token
1543
+ if (isFirstToken) {
1544
+ botBubble.innerHTML = "";
1545
+ isFirstToken = false;
1546
+ }
1547
+
1548
+ const chunk = message.data[0];
1549
+ botResponse += chunk;
1550
+
1551
+ botBubble.innerHTML = parseMarkdown(botResponse);
1552
+ addCopyButtons(botBubble);
1553
+ scrollToBottom();
1554
+ }
1555
+ }
1556
+
1557
+ // Complete interaction
1558
+ if (botResponse) {
1559
+ session.messages.push({ role: "assistant", content: botResponse });
1560
+ saveSessionsToStorage();
1561
+ renderSessions();
1562
+ } else {
1563
+ botBubble.textContent = "No response received from the model.";
1564
+ }
1565
+
1566
+ } catch (err) {
1567
+ console.error("Chat streaming error:", err);
1568
+ botBubble.innerHTML = `<span style="color: #ef4444;">System Error: ${err.message}</span>`;
1569
+ }
1570
+ }
1571
+
1572
+ function scrollToBottom() {
1573
+ messagesViewport.scrollTop = messagesViewport.scrollHeight;
1574
+ }
1575
+
1576
+ function escapeHtml(unsafe) {
1577
+ return unsafe
1578
+ .replace(/&/g, "&amp;")
1579
+ .replace(/</g, "&lt;")
1580
+ .replace(/>/g, "&gt;")
1581
+ .replace(/"/g, "&quot;")
1582
+ .replace(/'/g, "&#039;");
1583
+ }
1584
+
1585
+ // Load application
1586
+ window.addEventListener("DOMContentLoaded", init);
1587
+ </script>
1588
+ </body>
1589
+ </html>
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=6.16.0
2
+ openai
3
+ fastapi
4
+ uvicorn