elmarcito commited on
Commit
8958277
Β·
verified Β·
1 Parent(s): f9a4f6a

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +226 -305
static/index.html CHANGED
@@ -6,34 +6,11 @@
6
  <title>πŸš€ NVIDIA AI Multi-Model</title>
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>πŸš€</text></svg>">
8
  <style>
9
- :root{
10
- --nvidia:#76b900;
11
- --bg:#0a0e14;
12
- --card:#141a24;
13
- --card-hover:#1a2130;
14
- --border:#2a3341;
15
- --text:#e4e6eb;
16
- --text-dim:#8b95a7;
17
- --accent:#76b900;
18
- --accent-glow:rgba(118,185,0,.15);
19
- --error:#f44336;
20
- --code:#0d1117;
21
- }
22
  *{margin:0;padding:0;box-sizing:border-box}
23
  html,body{height:100%;overflow:hidden}
24
- body{
25
- font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
26
- background:var(--bg);color:var(--text);
27
- display:grid;grid-template-columns:280px 1fr 320px;
28
- grid-template-rows:60px 1fr;
29
- grid-template-areas:"header header header" "sidebar main config";
30
- }
31
- header{
32
- grid-area:header;
33
- background:linear-gradient(90deg,#0a0e14,#141a24);
34
- border-bottom:1px solid var(--border);
35
- display:flex;align-items:center;padding:0 20px;gap:20px;z-index:10;
36
- }
37
  .logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:18px}
38
  .logo-badge{background:var(--nvidia);color:#000;padding:3px 8px;border-radius:4px;font-size:11px;font-weight:800}
39
  .nav-tabs{display:flex;gap:5px;margin-left:20px}
@@ -44,7 +21,6 @@ header{
44
  .status{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim)}
45
  .status-dot{width:8px;height:8px;border-radius:50%;background:var(--nvidia);box-shadow:0 0 10px var(--nvidia);animation:pulse 2s infinite}
46
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
47
-
48
  .sidebar{grid-area:sidebar;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
49
  .sidebar-header{padding:16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
50
  .sidebar-title{font-size:12px;text-transform:uppercase;color:var(--text-dim);letter-spacing:1px}
@@ -58,7 +34,6 @@ header{
58
  .session-badge{background:var(--border);color:var(--text-dim);padding:2px 6px;border-radius:10px;font-size:10px}
59
  .session-delete{opacity:0;background:none;border:none;color:var(--error);cursor:pointer;font-size:14px;padding:0 4px}
60
  .session-item:hover .session-delete{opacity:1}
61
-
62
  .main{grid-area:main;display:flex;flex-direction:column;overflow:hidden}
63
  .chat-area{flex:1;overflow-y:auto;padding:24px 32px;scroll-behavior:smooth}
64
  .welcome{text-align:center;padding:60px 20px;color:var(--text-dim)}
@@ -69,7 +44,6 @@ header{
69
  .suggestion:hover{border-color:var(--accent);background:var(--card-hover);transform:translateY(-2px)}
70
  .suggestion-icon{font-size:24px;margin-bottom:8px}
71
  .suggestion-text{font-size:13px;color:var(--text)}
72
-
73
  .message{margin-bottom:20px;display:flex;gap:12px;animation:fadeIn .3s}
74
  @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
75
  .msg-avatar{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;font-size:14px}
@@ -86,10 +60,12 @@ header{
86
  .message:hover .msg-actions{opacity:1}
87
  .msg-btn{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px}
88
  .msg-btn:hover{color:var(--accent);background:var(--card)}
89
- .typing{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);animation:typing 1s infinite}
90
- @keyframes typing{0%,100%{opacity:.3}50%{opacity:1}}
 
 
 
91
  .error-msg{color:var(--error);padding:10px;background:rgba(244,67,54,.1);border-radius:8px;border:1px solid var(--error)}
92
-
93
  .input-area{padding:16px 32px 20px;border-top:1px solid var(--border);background:var(--bg)}
94
  .input-wrapper{display:flex;gap:8px;align-items:flex-end;background:var(--card);border:1px solid var(--border);border-radius:16px;padding:8px;transition:border-color .2s}
95
  .input-wrapper:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
@@ -100,7 +76,6 @@ header{
100
  .input-footer{display:flex;justify-content:space-between;margin-top:8px;font-size:11px;color:var(--text-dim);padding:0 8px}
101
  .stream-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
102
  .stream-toggle input{accent-color:var(--accent)}
103
-
104
  .config{grid-area:config;background:var(--card);border-left:1px solid var(--border);overflow-y:auto;padding:20px}
105
  .config-section{margin-bottom:24px}
106
  .config-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-dim);margin-bottom:8px;display:block;font-weight:600}
@@ -120,7 +95,8 @@ header{
120
  .stat-lbl{font-size:10px;color:var(--text-dim);text-transform:uppercase;margin-top:2px}
121
  .btn-icon{background:var(--card);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:13px;transition:all .2s;text-decoration:none;display:inline-flex;align-items:center;gap:6px}
122
  .btn-icon:hover{border-color:var(--accent);color:var(--accent)}
123
-
 
124
  .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;align-items:center;justify-content:center;z-index:100;padding:20px}
125
  .modal-overlay.active{display:flex}
126
  .modal{background:var(--card);border-radius:16px;max-width:900px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--border)}
@@ -128,53 +104,40 @@ header{
128
  .modal-title{font-size:18px;font-weight:700}
129
  .modal-close{background:none;border:none;color:var(--text);font-size:24px;cursor:pointer}
130
  .modal-body{padding:20px;overflow-y:auto;flex:1}
131
- .tab-bar{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:16px}
132
- .tab-btn{background:none;border:none;color:var(--text-dim);padding:10px 16px;cursor:pointer;font-size:13px;border-bottom:2px solid transparent;transition:all .2s}
133
- .tab-btn.active{color:var(--accent);border-color:var(--accent)}
134
- .tab-panel{display:none}
135
- .tab-panel.active{display:block}
136
  .code-block{background:var(--code);border:1px solid var(--border);border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:12px;color:#7ec8e3;overflow-x:auto;line-height:1.6;position:relative;margin:8px 0;white-space:pre-wrap}
137
  .copy-btn{position:absolute;top:8px;right:8px;background:var(--border);border:none;color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px}
138
  .copy-btn:hover{background:var(--accent);color:#000}
139
  .endpoint-table{width:100%;border-collapse:collapse;margin:8px 0;font-size:13px}
140
  .endpoint-table th,.endpoint-table td{padding:8px 10px;border:1px solid var(--border);text-align:left}
141
  .endpoint-table th{background:var(--code);color:var(--accent)}
142
- .method-badge{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700}
143
- .method-get{background:#1976d2;color:#fff}
144
- .method-post{background:var(--accent);color:#000}
145
-
146
- .toast{position:fixed;bottom:20px;right:20px;background:var(--card);border:1px solid var(--accent);color:var(--text);padding:12px 20px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.5);z-index:200;animation:slideIn .3s;display:none}
147
  .toast.show{display:block}
148
  .toast.error{border-color:var(--error);color:var(--error)}
149
  @keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
150
-
151
  ::-webkit-scrollbar{width:8px;height:8px}
152
  ::-webkit-scrollbar-track{background:var(--bg)}
153
  ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
154
  ::-webkit-scrollbar-thumb:hover{background:var(--accent)}
155
-
156
- @media(max-width:1100px){
157
- body{grid-template-columns:0 1fr 0}
158
- .sidebar,.config{display:none}
159
- }
160
  </style>
161
  </head>
162
  <body>
163
 
164
  <header>
165
- <div class="logo">
166
- <span>πŸš€</span><span>NVIDIA AI</span><span class="logo-badge">NIM</span>
167
- </div>
168
  <div class="nav-tabs">
169
  <div class="nav-tab active">πŸ’¬ Chat</div>
170
  <div class="nav-tab" onclick="openModal('models')">πŸ€– Modelli</div>
 
171
  <div class="nav-tab" onclick="openModal('api')">πŸ“– API</div>
172
  <div class="nav-tab" onclick="openModal('mcp')">πŸ”Œ MCP</div>
173
  <a class="nav-tab" href="/ui" target="_blank">🎨 Gradio</a>
174
  <a class="nav-tab" href="/docs" target="_blank">πŸ“š Swagger</a>
175
  </div>
176
  <div class="header-right">
177
- <div class="status"><div class="status-dot"></div><span id="statusText">Connesso</span></div>
178
  </div>
179
  </header>
180
 
@@ -187,31 +150,7 @@ header{
187
  </aside>
188
 
189
  <main class="main">
190
- <div class="chat-area" id="chatArea">
191
- <div class="welcome" id="welcome">
192
- <h1>πŸš€ NVIDIA AI Multi-Model</h1>
193
- <p>Chat con modelli NVIDIA NIM β€’ Memoria persistente β€’ SSE Streaming</p>
194
- <div class="suggestions">
195
- <div class="suggestion" onclick="sendSuggestion('Spiegami la meccanica quantistica in modo semplice')">
196
- <div class="suggestion-icon">πŸ”¬</div>
197
- <div class="suggestion-text">Spiegami la meccanica quantistica</div>
198
- </div>
199
- <div class="suggestion" onclick="sendSuggestion('Scrivi una funzione Python per il quicksort')">
200
- <div class="suggestion-icon">πŸ’»</div>
201
- <div class="suggestion-text">Scrivi una funzione Python per il quicksort</div>
202
- </div>
203
- <div class="suggestion" onclick="sendSuggestion('Raccontami una storia breve sci-fi in italiano')">
204
- <div class="suggestion-icon">πŸ“–</div>
205
- <div class="suggestion-text">Raccontami una storia sci-fi</div>
206
- </div>
207
- <div class="suggestion" onclick="sendSuggestion('Confronta React vs Vue vs Svelte')">
208
- <div class="suggestion-icon">⚑</div>
209
- <div class="suggestion-text">Confronta React vs Vue vs Svelte</div>
210
- </div>
211
- </div>
212
- </div>
213
- </div>
214
-
215
  <div class="input-area">
216
  <div class="input-wrapper">
217
  <textarea id="userInput" placeholder="Scrivi un messaggio... (Shift+Enter per nuova riga)" rows="1"></textarea>
@@ -232,13 +171,12 @@ header{
232
  <label class="config-label">πŸ€– Modello</label>
233
  <select class="config-select" id="modelSelect"></select>
234
  <div class="model-info" id="modelInfo"></div>
 
235
  </div>
236
-
237
  <div class="config-section">
238
  <label class="config-label">πŸ“ System Prompt</label>
239
  <textarea class="config-textarea" id="systemPrompt">Sei un assistente AI utile. Rispondi in italiano se l'utente scrive in italiano.</textarea>
240
  </div>
241
-
242
  <div class="config-section">
243
  <label class="config-label">🌑️ Temperature</label>
244
  <div class="slider-row">
@@ -246,7 +184,6 @@ header{
246
  <span class="slider-value" id="temperatureVal">0.7</span>
247
  </div>
248
  </div>
249
-
250
  <div class="config-section">
251
  <label class="config-label">πŸ“ Max Tokens</label>
252
  <div class="slider-row">
@@ -254,7 +191,6 @@ header{
254
  <span class="slider-value" id="maxTokensVal">4096</span>
255
  </div>
256
  </div>
257
-
258
  <div class="config-section">
259
  <label class="config-label">🧠 Memoria</label>
260
  <label class="checkbox-row">
@@ -262,9 +198,8 @@ header{
262
  <span>Attiva memoria breve/lunga</span>
263
  </label>
264
  </div>
265
-
266
  <div class="config-section">
267
- <label class="config-label">πŸ“Š Statistiche Sessione</label>
268
  <div class="stats-grid">
269
  <div class="stat"><div class="stat-val" id="statShort">0</div><div class="stat-lbl">Breve</div></div>
270
  <div class="stat"><div class="stat-val" id="statLong">0</div><div class="stat-lbl">Lunga</div></div>
@@ -272,7 +207,6 @@ header{
272
  <div class="stat"><div class="stat-val" id="statTotal">0</div><div class="stat-lbl">Totali</div></div>
273
  </div>
274
  </div>
275
-
276
  <div class="config-section">
277
  <button class="btn-new" style="width:100%;margin-bottom:8px" onclick="clearCurrentSession()">πŸ—‘οΈ Pulisci Chat</button>
278
  <button class="btn-icon" style="width:100%;justify-content:center" onclick="exportChat()">πŸ’Ύ Esporta</button>
@@ -292,9 +226,7 @@ header{
292
  <div class="toast" id="toast"></div>
293
 
294
  <script>
295
- // ═══════════════════════════════════════════════════════
296
- // STATE
297
- // ═══════════════════════════════════════════════════════
298
  const DEFAULT_MODEL = 'meta/llama-3.1-8b-instruct';
299
 
300
  const state = {
@@ -323,10 +255,8 @@ async function loadModels(){
323
  const data = await r.json();
324
  state.models = data.models;
325
  state.defaultModel = data.default || DEFAULT_MODEL;
326
-
327
  const sel = document.getElementById('modelSelect');
328
  sel.innerHTML = '';
329
-
330
  const grouped = {};
331
  for(const [id,info] of Object.entries(data.models)){
332
  const cat = info.category;
@@ -344,8 +274,6 @@ async function loadModels(){
344
  }
345
  sel.appendChild(og);
346
  }
347
-
348
- // Imposta il modello: prima localStorage, poi default dal server, poi fallback
349
  const stored = localStorage.getItem('selectedModel');
350
  if(stored && state.models[stored]){
351
  sel.value = stored;
@@ -353,17 +281,16 @@ async function loadModels(){
353
  sel.value = state.defaultModel;
354
  localStorage.setItem('selectedModel', state.defaultModel);
355
  }
356
-
357
  sel.addEventListener('change', ()=>{
358
  localStorage.setItem('selectedModel', sel.value);
359
  updateModelInfo();
360
  showToast('βœ… Modello: ' + state.models[sel.value].name);
361
  });
362
  updateModelInfo();
363
- console.log('βœ… Modelli caricati:', Object.keys(data.models).length, '| Default:', sel.value);
364
  }catch(e){
365
- console.error('❌ Errore caricamento modelli:', e);
366
- showToast('❌ Errore caricamento modelli', true);
367
  }
368
  }
369
 
@@ -371,12 +298,11 @@ function updateModelInfo(){
371
  const id = document.getElementById('modelSelect').value;
372
  const m = state.models[id];
373
  if(!m) return;
374
- document.getElementById('modelInfo').innerHTML = `
375
- <b>ID:</b> ${id}<br>
376
- <b>Categoria:</b> ${m.category}<br>
377
- <b>Context:</b> ${m.context.toLocaleString()} token<br>
378
- <b>${m.description}</b>
379
- `;
380
  }
381
 
382
  // ═══════════════════════════════════════════════════════
@@ -396,12 +322,15 @@ function renderSessions(){
396
  for(const [id, s] of Object.entries(state.sessions)){
397
  const div = document.createElement('div');
398
  div.className = 'session-item' + (id === state.currentSession ? ' active' : '');
399
- div.innerHTML = `
400
- <span class="session-name">πŸ’¬ ${escapeHtml(s.name)}</span>
401
- <span class="session-badge">${s.messages.length}</span>
402
- <button class="session-delete" onclick="event.stopPropagation();deleteSession('${escapeHtml(id)}')">πŸ—‘οΈ</button>
403
- `;
404
- div.onclick = ()=>switchSession(id);
 
 
 
405
  list.appendChild(div);
406
  }
407
  }
@@ -427,7 +356,7 @@ function switchSession(id){
427
  }
428
 
429
  async function deleteSession(id){
430
- if(!confirm(`Eliminare sessione "${id}"?`)) return;
431
  delete state.sessions[id];
432
  try{ await fetch('/v1/sessions/' + encodeURIComponent(id), {method:'DELETE'}); }catch(e){}
433
  if(id === state.currentSession){
@@ -445,15 +374,12 @@ async function deleteSession(id){
445
  }
446
 
447
  function saveSessions(){
448
- try{
449
- localStorage.setItem('sessions', JSON.stringify(state.sessions));
450
- }catch(e){
451
- console.warn('LocalStorage pieno, pulizia messaggi vecchi');
452
- }
453
  }
454
 
455
  async function clearCurrentSession(){
456
- if(!confirm('Pulire tutti i messaggi di questa sessione?')) return;
457
  state.sessions[state.currentSession].messages = [];
458
  try{
459
  await fetch('/v1/memory', {
@@ -466,27 +392,27 @@ async function clearCurrentSession(){
466
  loadCurrentChat();
467
  updateStats();
468
  renderSessions();
469
- showToast('πŸ—‘οΈ Chat pulita');
470
  }
471
 
472
  // ═══════════════════════════════════════════════════════
473
- // CHAT
474
  // ═══════════════════════════════════════════════════════
475
  function loadCurrentChat(){
476
  const area = document.getElementById('chatArea');
477
  const msgs = state.sessions[state.currentSession].messages;
478
  area.innerHTML = '';
479
  if(msgs.length === 0){
480
- area.innerHTML = `<div class="welcome" id="welcome">
481
- <h1>πŸš€ NVIDIA AI Multi-Model</h1>
482
- <p>Sessione: <b>${escapeHtml(state.currentSession)}</b></p>
483
- <div class="suggestions">
484
- <div class="suggestion" onclick="sendSuggestion('Ciao! Chi sei?')"><div class="suggestion-icon">πŸ‘‹</div><div class="suggestion-text">Ciao! Chi sei?</div></div>
485
- <div class="suggestion" onclick="sendSuggestion('Aiutami a scrivere codice Python')"><div class="suggestion-icon">πŸ’»</div><div class="suggestion-text">Aiutami con codice Python</div></div>
486
- <div class="suggestion" onclick="sendSuggestion('Spiegami come funziona la memoria di questa chat')"><div class="suggestion-icon">🧠</div><div class="suggestion-text">Come funziona la memoria?</div></div>
487
- <div class="suggestion" onclick="sendSuggestion('Scrivi una poesia in italiano')"><div class="suggestion-icon">πŸ“</div><div class="suggestion-text">Scrivi una poesia</div></div>
488
- </div>
489
- </div>`;
490
  return;
491
  }
492
  msgs.forEach(m => appendMessage(m.role, m.content, false));
@@ -494,42 +420,78 @@ function loadCurrentChat(){
494
 
495
  function appendMessage(role, content, save=true){
496
  const area = document.getElementById('chatArea');
497
- const welcome = document.getElementById('welcome');
498
  if(welcome) welcome.remove();
 
499
  const div = document.createElement('div');
500
  div.className = 'message';
501
  const isUser = role === 'user';
502
  const currentModel = document.getElementById('modelSelect').value;
503
  const modelName = state.models[currentModel]?.name || 'Assistente';
504
- div.innerHTML = `
505
- <div class="msg-avatar ${role}">${isUser ? 'πŸ‘€' : 'πŸ€–'}</div>
506
- <div class="msg-content">
507
- <div class="msg-role">${isUser ? 'Tu' : escapeHtml(modelName)}</div>
508
- <div class="msg-text ${role}">${renderMarkdown(content)}</div>
509
- <div class="msg-actions">
510
- <button class="msg-btn" onclick="copyText(this)">πŸ“‹ Copia</button>
511
- </div>
512
- </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  area.appendChild(div);
514
  area.scrollTop = area.scrollHeight;
 
515
  if(save){
516
  state.sessions[state.currentSession].messages.push({role, content, timestamp: Date.now()});
517
  saveSessions();
518
  renderSessions();
519
  }
520
- return div.querySelector('.msg-text');
 
 
 
 
521
  }
522
 
523
  function escapeHtml(text){
524
  const div = document.createElement('div');
525
- div.textContent = String(text);
526
  return div.innerHTML;
527
  }
528
 
 
 
 
 
529
  function renderMarkdown(text){
 
530
  text = escapeHtml(text);
531
  text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_,lang,code)=>
532
- `<pre><code class="lang-${lang||''}">${code.trim()}</code></pre>`);
533
  text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>');
534
  text = text.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
535
  text = text.replace(/\*([^*\n]+)\*/g, '<i>$1</i>');
@@ -537,12 +499,6 @@ function renderMarkdown(text){
537
  return text;
538
  }
539
 
540
- function copyText(btn){
541
- const text = btn.closest('.msg-content').querySelector('.msg-text').innerText;
542
- navigator.clipboard.writeText(text);
543
- showToast('βœ… Copiato');
544
- }
545
-
546
  function sendSuggestion(text){
547
  document.getElementById('userInput').value = text;
548
  sendMessage();
@@ -553,15 +509,13 @@ async function sendMessage(){
553
  const message = input.value.trim();
554
  if(!message || state.isGenerating) return;
555
 
556
- // ─── VALIDAZIONE MODELLO ───
557
  const modelSel = document.getElementById('modelSelect');
558
  let model = modelSel.value;
559
  if(!model || model.trim() === ''){
560
  model = localStorage.getItem('selectedModel') || state.defaultModel || DEFAULT_MODEL;
561
  modelSel.value = model;
562
- console.warn('⚠️ Modello mancante, uso fallback:', model);
563
  }
564
- console.log('πŸ“€ Sending β†’ model:', model, '| session:', state.currentSession);
565
 
566
  const systemPrompt = document.getElementById('systemPrompt').value || 'Sei un assistente utile.';
567
  const temperature = parseFloat(document.getElementById('temperature').value);
@@ -576,7 +530,8 @@ async function sendMessage(){
576
  state.isGenerating = true;
577
  document.getElementById('sendBtn').disabled = true;
578
 
579
- const assistDiv = appendMessage('assistant', '<span class="typing"></span>', false);
 
580
  let fullText = '';
581
 
582
  try{
@@ -589,10 +544,10 @@ async function sendMessage(){
589
  use_memory: useMemory
590
  }, (chunk)=>{
591
  fullText += chunk;
592
- assistDiv.innerHTML = renderMarkdown(fullText) + '<span class="typing"></span>';
593
  document.getElementById('chatArea').scrollTop = document.getElementById('chatArea').scrollHeight;
594
  });
595
- assistDiv.innerHTML = renderMarkdown(fullText);
596
  } else {
597
  const r = await fetch('/v1/chat', {
598
  method:'POST',
@@ -606,11 +561,9 @@ async function sendMessage(){
606
  })
607
  });
608
  const data = await r.json();
609
- if(!r.ok){
610
- throw new Error(data.detail || data.error || 'Errore server');
611
- }
612
  fullText = data.response || '';
613
- assistDiv.innerHTML = renderMarkdown(fullText);
614
  }
615
 
616
  if(fullText){
@@ -622,8 +575,8 @@ async function sendMessage(){
622
  updateStats();
623
  }
624
  }catch(e){
625
- console.error('❌ Chat error:', e);
626
- assistDiv.innerHTML = `<div class="error-msg">❌ ${escapeHtml(e.message)}<br><br>πŸ’‘ Prova un altro modello (es. LLaMA 3.1 8B)</div>`;
627
  } finally {
628
  state.isGenerating = false;
629
  document.getElementById('sendBtn').disabled = false;
@@ -633,10 +586,8 @@ async function sendMessage(){
633
 
634
  async function streamChat(params, onChunk){
635
  return new Promise((resolve, reject)=>{
636
- // Validazione + fallback
637
  const model = params.model || localStorage.getItem('selectedModel') || DEFAULT_MODEL;
638
- console.log('πŸš€ SSE Streaming β†’ model:', model, '| session:', params.session_id);
639
-
640
  const qs = new URLSearchParams({
641
  message: String(params.message || ''),
642
  model: String(model),
@@ -646,65 +597,66 @@ async function streamChat(params, onChunk){
646
  max_tokens: String(params.max_tokens || 4096),
647
  use_memory: String(params.use_memory !== false)
648
  });
649
-
650
  const es = new EventSource('/v1/chat/stream?' + qs.toString());
651
  state.currentEventSource = es;
652
-
653
- let receivedAnyToken = false;
654
  let hadError = false;
655
 
656
- es.addEventListener('start', (e)=>{
657
- console.log('▢️ SSE start:', e.data);
658
- });
659
-
660
  es.addEventListener('token', (e)=>{
661
  try{
662
  const d = JSON.parse(e.data);
663
- if(d.token){
664
- receivedAnyToken = true;
665
- onChunk(d.token);
666
- }
667
- }catch(err){ console.error('token parse error', err); }
668
  });
669
-
670
  es.addEventListener('done', (e)=>{
671
- console.log('βœ… SSE done');
672
  es.close();
673
  state.currentEventSource = null;
674
  resolve();
675
  });
676
-
677
  es.addEventListener('error', (e)=>{
678
- console.error('❌ SSE error event, data:', e.data);
679
  hadError = true;
680
  es.close();
681
  state.currentEventSource = null;
682
-
683
  if(e.data){
684
- try{
685
- const err = JSON.parse(e.data);
686
- reject(new Error(err.error || 'Errore stream'));
687
- }catch{
688
- reject(new Error('Errore stream (parsing fallito)'));
689
- }
690
- } else if(receivedAnyToken){
691
- // Se abbiamo ricevuto qualcosa, considera completato
692
  resolve();
693
  } else {
694
- reject(new Error('Connessione SSE fallita. Verifica che il modello ' + model + ' sia disponibile. Apri F12 β†’ Console per dettagli.'));
695
  }
696
  });
697
-
698
- // Safety timeout
699
  setTimeout(()=>{
700
- if(!receivedAnyToken && !hadError){
701
  es.close();
702
- reject(new Error('Timeout: nessuna risposta dopo 60s. Il modello potrebbe non essere disponibile.'));
703
  }
704
  }, 60000);
705
  });
706
  }
707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  // ═══════════════════════════════════════════════════════
709
  // STATS
710
  // ═══════════════════════════════════════════════════════
@@ -747,15 +699,14 @@ function updateTokenCount(){
747
  // ═══════════════════════════════════════════════════════
748
  function exportChat(){
749
  const s = state.sessions[state.currentSession];
750
- const data = JSON.stringify(s, null, 2);
751
- const blob = new Blob([data], {type:'application/json'});
752
  const url = URL.createObjectURL(blob);
753
  const a = document.createElement('a');
754
  a.href = url;
755
- a.download = `chat-${state.currentSession}-${Date.now()}.json`;
756
  a.click();
757
  URL.revokeObjectURL(url);
758
- showToast('βœ… Chat esportata');
759
  }
760
 
761
  // ═══════════════════════════════════════════════════════
@@ -765,126 +716,100 @@ function openModal(type){
765
  document.getElementById('modal').classList.add('active');
766
  const title = document.getElementById('modalTitle');
767
  const body = document.getElementById('modalBody');
 
768
 
769
  if(type === 'models'){
770
- title.textContent = 'πŸ€– Catalogo Modelli NVIDIA NIM';
771
- let html = `<p style="margin-bottom:12px;color:var(--text-dim)">Totale: <b>${Object.keys(state.models).length}</b> modelli</p>`;
772
- html += '<table class="endpoint-table"><tr><th>Categoria</th><th>Nome</th><th>Model ID</th><th>Context</th></tr>';
773
  for(const [id,m] of Object.entries(state.models)){
774
- html += `<tr><td>${escapeHtml(m.category)}</td><td>${escapeHtml(m.name)}</td><td><code>${escapeHtml(id)}</code></td><td>${m.context.toLocaleString()}</td></tr>`;
775
  }
776
  html += '</table>';
777
  body.innerHTML = html;
778
- } else if(type === 'api'){
 
 
 
 
 
 
 
 
 
 
779
  title.textContent = 'πŸ“– API Reference';
780
- const base = location.origin;
781
- body.innerHTML = `
782
- <div class="tab-bar">
783
- <button class="tab-btn active" onclick="switchTab('curl',this)">cURL</button>
784
- <button class="tab-btn" onclick="switchTab('python',this)">Python</button>
785
- <button class="tab-btn" onclick="switchTab('js',this)">JavaScript</button>
786
- <button class="tab-btn" onclick="switchTab('sse',this)">SSE</button>
787
- </div>
788
- <div class="tab-panel active" id="tab-curl">
789
- <h3>Chat GET</h3>
790
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl "${base}/v1/chat/get?message=Ciao&model=meta/llama-3.1-8b-instruct&session_id=test"</div>
791
- <h3>Chat POST</h3>
792
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/chat \\
793
- -H "Content-Type: application/json" \\
794
- -d '{"message":"Ciao!","model":"meta/llama-3.1-8b-instruct","session_id":"test"}'</div>
795
- <h3>SSE Streaming</h3>
796
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -N "${base}/v1/chat/stream?message=Raccontami+una+storia&model=meta/llama-3.1-8b-instruct"</div>
797
- <h3>Lista Modelli</h3>
798
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl ${base}/v1/models</div>
799
- </div>
800
- <div class="tab-panel" id="tab-python">
801
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>import requests
802
-
803
- r = requests.post("${base}/v1/chat", json={
804
- "message": "Ciao!",
805
- "model": "meta/llama-3.1-8b-instruct",
806
- "session_id": "python-test",
807
- "use_memory": True
808
- })
809
- print(r.json()["response"])</div>
810
- <h3>Streaming SSE con httpx</h3>
811
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>import httpx, json
812
-
813
- with httpx.stream('GET', '${base}/v1/chat/stream',
814
- params={'message':'Ciao!','model':'meta/llama-3.1-8b-instruct','session_id':'py'}) as r:
815
- for line in r.iter_lines():
816
- if line.startswith('data: '):
817
- d = json.loads(line[6:])
818
- if 'token' in d: print(d['token'], end='', flush=True)</div>
819
- </div>
820
- <div class="tab-panel" id="tab-js">
821
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>const r = await fetch('${base}/v1/chat', {
822
- method: 'POST',
823
- headers: {'Content-Type':'application/json'},
824
- body: JSON.stringify({
825
- message: 'Hello!',
826
- model: 'meta/llama-3.1-8b-instruct',
827
- session_id: 'js-test'
828
- })
829
- });
830
- const data = await r.json();
831
- console.log(data.response);</div>
832
- </div>
833
- <div class="tab-panel" id="tab-sse">
834
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>const es = new EventSource(
835
- '${base}/v1/chat/stream?message=Ciao&model=meta/llama-3.1-8b-instruct&session_id=sse'
836
- );
837
- es.addEventListener('token', (e) => {
838
- const {token} = JSON.parse(e.data);
839
- document.body.innerHTML += token;
840
- });
841
- es.addEventListener('done', (e) => {
842
- console.log('Fine:', JSON.parse(e.data));
843
- es.close();
844
- });</div>
845
- </div>
846
- `;
847
- } else if(type === 'mcp'){
848
  title.textContent = 'πŸ”Œ MCP Server';
849
- const base = location.origin;
850
- body.innerHTML = `
851
- <h3>Configurazione Claude Desktop / Cursor</h3>
852
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>{
853
- "mcpServers": {
854
- "nvidia-ai": {
855
- "url": "${base}/v1/mcp",
856
- "transport": "http"
857
- }
 
 
 
 
 
858
  }
859
- }</div>
860
-
861
- <h3>Test Initialize</h3>
862
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
863
- -H "Content-Type: application/json" \\
864
- -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'</div>
865
-
866
- <h3>Lista Tools</h3>
867
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
868
- -H "Content-Type: application/json" \\
869
- -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'</div>
870
-
871
- <h3>Chiamata Tool</h3>
872
- <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
873
- -H "Content-Type: application/json" \\
874
- -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"nvidia_chat","arguments":{"message":"Ciao!","model":"meta/llama-3.1-8b-instruct"}}}'</div>
875
- `;
 
 
 
 
 
 
 
 
876
  }
877
  }
878
 
879
- function switchTab(id, btn){
880
- document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
881
- document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
882
- btn.classList.add('active');
883
- document.getElementById('tab-'+id).classList.add('active');
 
 
 
 
 
 
 
 
 
 
884
  }
885
 
886
  function copyBlock(btn){
887
- const text = btn.parentElement.innerText.replace('Copia','').trim();
888
  navigator.clipboard.writeText(text);
889
  btn.textContent = 'βœ…';
890
  setTimeout(()=>btn.textContent='Copia', 1500);
@@ -898,9 +823,6 @@ document.getElementById('modal').addEventListener('click', (e)=>{
898
  if(e.target.id === 'modal') closeModal();
899
  });
900
 
901
- // ═══════════════════════════════════════════════════════
902
- // TOAST
903
- // ═══════════════════════════════════════════════════════
904
  function showToast(msg, isError=false){
905
  const t = document.getElementById('toast');
906
  t.textContent = msg;
@@ -908,7 +830,6 @@ function showToast(msg, isError=false){
908
  setTimeout(()=>t.classList.remove('show'), 3000);
909
  }
910
 
911
- // GO!
912
  init();
913
  </script>
914
  </body>
 
6
  <title>πŸš€ NVIDIA AI Multi-Model</title>
7
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>πŸš€</text></svg>">
8
  <style>
9
+ :root{--nvidia:#76b900;--bg:#0a0e14;--card:#141a24;--card-hover:#1a2130;--border:#2a3341;--text:#e4e6eb;--text-dim:#8b95a7;--accent:#76b900;--accent-glow:rgba(118,185,0,.15);--error:#f44336;--code:#0d1117;}
 
 
 
 
 
 
 
 
 
 
 
 
10
  *{margin:0;padding:0;box-sizing:border-box}
11
  html,body{height:100%;overflow:hidden}
12
+ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);display:grid;grid-template-columns:280px 1fr 320px;grid-template-rows:60px 1fr;grid-template-areas:"header header header" "sidebar main config"}
13
+ header{grid-area:header;background:linear-gradient(90deg,#0a0e14,#141a24);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 20px;gap:20px;z-index:10}
 
 
 
 
 
 
 
 
 
 
 
14
  .logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:18px}
15
  .logo-badge{background:var(--nvidia);color:#000;padding:3px 8px;border-radius:4px;font-size:11px;font-weight:800}
16
  .nav-tabs{display:flex;gap:5px;margin-left:20px}
 
21
  .status{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim)}
22
  .status-dot{width:8px;height:8px;border-radius:50%;background:var(--nvidia);box-shadow:0 0 10px var(--nvidia);animation:pulse 2s infinite}
23
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
 
24
  .sidebar{grid-area:sidebar;background:var(--card);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
25
  .sidebar-header{padding:16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
26
  .sidebar-title{font-size:12px;text-transform:uppercase;color:var(--text-dim);letter-spacing:1px}
 
34
  .session-badge{background:var(--border);color:var(--text-dim);padding:2px 6px;border-radius:10px;font-size:10px}
35
  .session-delete{opacity:0;background:none;border:none;color:var(--error);cursor:pointer;font-size:14px;padding:0 4px}
36
  .session-item:hover .session-delete{opacity:1}
 
37
  .main{grid-area:main;display:flex;flex-direction:column;overflow:hidden}
38
  .chat-area{flex:1;overflow-y:auto;padding:24px 32px;scroll-behavior:smooth}
39
  .welcome{text-align:center;padding:60px 20px;color:var(--text-dim)}
 
44
  .suggestion:hover{border-color:var(--accent);background:var(--card-hover);transform:translateY(-2px)}
45
  .suggestion-icon{font-size:24px;margin-bottom:8px}
46
  .suggestion-text{font-size:13px;color:var(--text)}
 
47
  .message{margin-bottom:20px;display:flex;gap:12px;animation:fadeIn .3s}
48
  @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
49
  .msg-avatar{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;flex-shrink:0;font-size:14px}
 
60
  .message:hover .msg-actions{opacity:1}
61
  .msg-btn{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px}
62
  .msg-btn:hover{color:var(--accent);background:var(--card)}
63
+ .typing-dots{display:inline-flex;gap:4px;align-items:center;padding:8px 0}
64
+ .typing-dots span{width:8px;height:8px;border-radius:50%;background:var(--accent);animation:typing 1.4s infinite}
65
+ .typing-dots span:nth-child(2){animation-delay:.2s}
66
+ .typing-dots span:nth-child(3){animation-delay:.4s}
67
+ @keyframes typing{0%,60%,100%{opacity:.3;transform:translateY(0)}30%{opacity:1;transform:translateY(-4px)}}
68
  .error-msg{color:var(--error);padding:10px;background:rgba(244,67,54,.1);border-radius:8px;border:1px solid var(--error)}
 
69
  .input-area{padding:16px 32px 20px;border-top:1px solid var(--border);background:var(--bg)}
70
  .input-wrapper{display:flex;gap:8px;align-items:flex-end;background:var(--card);border:1px solid var(--border);border-radius:16px;padding:8px;transition:border-color .2s}
71
  .input-wrapper:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
 
76
  .input-footer{display:flex;justify-content:space-between;margin-top:8px;font-size:11px;color:var(--text-dim);padding:0 8px}
77
  .stream-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
78
  .stream-toggle input{accent-color:var(--accent)}
 
79
  .config{grid-area:config;background:var(--card);border-left:1px solid var(--border);overflow-y:auto;padding:20px}
80
  .config-section{margin-bottom:24px}
81
  .config-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-dim);margin-bottom:8px;display:block;font-weight:600}
 
95
  .stat-lbl{font-size:10px;color:var(--text-dim);text-transform:uppercase;margin-top:2px}
96
  .btn-icon{background:var(--card);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:13px;transition:all .2s;text-decoration:none;display:inline-flex;align-items:center;gap:6px}
97
  .btn-icon:hover{border-color:var(--accent);color:var(--accent)}
98
+ .btn-test{background:var(--accent);color:#000;border:none;padding:8px 12px;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;width:100%;margin-top:8px}
99
+ .btn-test:hover{background:#8ad100}
100
  .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:none;align-items:center;justify-content:center;z-index:100;padding:20px}
101
  .modal-overlay.active{display:flex}
102
  .modal{background:var(--card);border-radius:16px;max-width:900px;width:100%;max-height:85vh;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--border)}
 
104
  .modal-title{font-size:18px;font-weight:700}
105
  .modal-close{background:none;border:none;color:var(--text);font-size:24px;cursor:pointer}
106
  .modal-body{padding:20px;overflow-y:auto;flex:1}
 
 
 
 
 
107
  .code-block{background:var(--code);border:1px solid var(--border);border-radius:8px;padding:12px;font-family:'Fira Code',monospace;font-size:12px;color:#7ec8e3;overflow-x:auto;line-height:1.6;position:relative;margin:8px 0;white-space:pre-wrap}
108
  .copy-btn{position:absolute;top:8px;right:8px;background:var(--border);border:none;color:var(--text);padding:4px 10px;border-radius:4px;cursor:pointer;font-size:11px}
109
  .copy-btn:hover{background:var(--accent);color:#000}
110
  .endpoint-table{width:100%;border-collapse:collapse;margin:8px 0;font-size:13px}
111
  .endpoint-table th,.endpoint-table td{padding:8px 10px;border:1px solid var(--border);text-align:left}
112
  .endpoint-table th{background:var(--code);color:var(--accent)}
113
+ .test-loading{text-align:center;padding:40px;color:var(--text-dim)}
114
+ .test-loading .typing-dots{justify-content:center}
115
+ .toast{position:fixed;bottom:20px;right:20px;background:var(--card);border:1px solid var(--accent);color:var(--text);padding:12px 20px;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,.5);z-index:200;animation:slideIn .3s;display:none;max-width:400px}
 
 
116
  .toast.show{display:block}
117
  .toast.error{border-color:var(--error);color:var(--error)}
118
  @keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
 
119
  ::-webkit-scrollbar{width:8px;height:8px}
120
  ::-webkit-scrollbar-track{background:var(--bg)}
121
  ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
122
  ::-webkit-scrollbar-thumb:hover{background:var(--accent)}
123
+ @media(max-width:1100px){body{grid-template-columns:0 1fr 0}.sidebar,.config{display:none}}
 
 
 
 
124
  </style>
125
  </head>
126
  <body>
127
 
128
  <header>
129
+ <div class="logo"><span>πŸš€</span><span>NVIDIA AI</span><span class="logo-badge">NIM</span></div>
 
 
130
  <div class="nav-tabs">
131
  <div class="nav-tab active">πŸ’¬ Chat</div>
132
  <div class="nav-tab" onclick="openModal('models')">πŸ€– Modelli</div>
133
+ <div class="nav-tab" onclick="openModal('test')">πŸš€ Test Parallelo</div>
134
  <div class="nav-tab" onclick="openModal('api')">πŸ“– API</div>
135
  <div class="nav-tab" onclick="openModal('mcp')">πŸ”Œ MCP</div>
136
  <a class="nav-tab" href="/ui" target="_blank">🎨 Gradio</a>
137
  <a class="nav-tab" href="/docs" target="_blank">πŸ“š Swagger</a>
138
  </div>
139
  <div class="header-right">
140
+ <div class="status"><div class="status-dot"></div><span>Connesso</span></div>
141
  </div>
142
  </header>
143
 
 
150
  </aside>
151
 
152
  <main class="main">
153
+ <div class="chat-area" id="chatArea"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  <div class="input-area">
155
  <div class="input-wrapper">
156
  <textarea id="userInput" placeholder="Scrivi un messaggio... (Shift+Enter per nuova riga)" rows="1"></textarea>
 
171
  <label class="config-label">πŸ€– Modello</label>
172
  <select class="config-select" id="modelSelect"></select>
173
  <div class="model-info" id="modelInfo"></div>
174
+ <button class="btn-test" onclick="testCurrentModel()">πŸ§ͺ Testa modello corrente</button>
175
  </div>
 
176
  <div class="config-section">
177
  <label class="config-label">πŸ“ System Prompt</label>
178
  <textarea class="config-textarea" id="systemPrompt">Sei un assistente AI utile. Rispondi in italiano se l'utente scrive in italiano.</textarea>
179
  </div>
 
180
  <div class="config-section">
181
  <label class="config-label">🌑️ Temperature</label>
182
  <div class="slider-row">
 
184
  <span class="slider-value" id="temperatureVal">0.7</span>
185
  </div>
186
  </div>
 
187
  <div class="config-section">
188
  <label class="config-label">πŸ“ Max Tokens</label>
189
  <div class="slider-row">
 
191
  <span class="slider-value" id="maxTokensVal">4096</span>
192
  </div>
193
  </div>
 
194
  <div class="config-section">
195
  <label class="config-label">🧠 Memoria</label>
196
  <label class="checkbox-row">
 
198
  <span>Attiva memoria breve/lunga</span>
199
  </label>
200
  </div>
 
201
  <div class="config-section">
202
+ <label class="config-label">πŸ“Š Statistiche</label>
203
  <div class="stats-grid">
204
  <div class="stat"><div class="stat-val" id="statShort">0</div><div class="stat-lbl">Breve</div></div>
205
  <div class="stat"><div class="stat-val" id="statLong">0</div><div class="stat-lbl">Lunga</div></div>
 
207
  <div class="stat"><div class="stat-val" id="statTotal">0</div><div class="stat-lbl">Totali</div></div>
208
  </div>
209
  </div>
 
210
  <div class="config-section">
211
  <button class="btn-new" style="width:100%;margin-bottom:8px" onclick="clearCurrentSession()">πŸ—‘οΈ Pulisci Chat</button>
212
  <button class="btn-icon" style="width:100%;justify-content:center" onclick="exportChat()">πŸ’Ύ Esporta</button>
 
226
  <div class="toast" id="toast"></div>
227
 
228
  <script>
229
+ 'use strict';
 
 
230
  const DEFAULT_MODEL = 'meta/llama-3.1-8b-instruct';
231
 
232
  const state = {
 
255
  const data = await r.json();
256
  state.models = data.models;
257
  state.defaultModel = data.default || DEFAULT_MODEL;
 
258
  const sel = document.getElementById('modelSelect');
259
  sel.innerHTML = '';
 
260
  const grouped = {};
261
  for(const [id,info] of Object.entries(data.models)){
262
  const cat = info.category;
 
274
  }
275
  sel.appendChild(og);
276
  }
 
 
277
  const stored = localStorage.getItem('selectedModel');
278
  if(stored && state.models[stored]){
279
  sel.value = stored;
 
281
  sel.value = state.defaultModel;
282
  localStorage.setItem('selectedModel', state.defaultModel);
283
  }
 
284
  sel.addEventListener('change', ()=>{
285
  localStorage.setItem('selectedModel', sel.value);
286
  updateModelInfo();
287
  showToast('βœ… Modello: ' + state.models[sel.value].name);
288
  });
289
  updateModelInfo();
290
+ console.log('βœ… Modelli:', Object.keys(data.models).length, '| Default:', sel.value);
291
  }catch(e){
292
+ console.error('❌', e);
293
+ showToast('❌ Errore modelli', true);
294
  }
295
  }
296
 
 
298
  const id = document.getElementById('modelSelect').value;
299
  const m = state.models[id];
300
  if(!m) return;
301
+ document.getElementById('modelInfo').innerHTML =
302
+ '<b>ID:</b> ' + escapeHtml(id) + '<br>' +
303
+ '<b>Categoria:</b> ' + escapeHtml(m.category) + '<br>' +
304
+ '<b>Context:</b> ' + m.context.toLocaleString() + ' token<br>' +
305
+ '<b>' + escapeHtml(m.description) + '</b>';
 
306
  }
307
 
308
  // ═══════════════════════════════════════════════════════
 
322
  for(const [id, s] of Object.entries(state.sessions)){
323
  const div = document.createElement('div');
324
  div.className = 'session-item' + (id === state.currentSession ? ' active' : '');
325
+ div.innerHTML =
326
+ '<span class="session-name">πŸ’¬ ' + escapeHtml(s.name) + '</span>' +
327
+ '<span class="session-badge">' + s.messages.length + '</span>' +
328
+ '<button class="session-delete" data-sid="' + escapeAttr(id) + '">πŸ—‘οΈ</button>';
329
+ div.querySelector('.session-delete').addEventListener('click', (e)=>{
330
+ e.stopPropagation();
331
+ deleteSession(id);
332
+ });
333
+ div.addEventListener('click', ()=>switchSession(id));
334
  list.appendChild(div);
335
  }
336
  }
 
356
  }
357
 
358
  async function deleteSession(id){
359
+ if(!confirm('Eliminare sessione "' + id + '"?')) return;
360
  delete state.sessions[id];
361
  try{ await fetch('/v1/sessions/' + encodeURIComponent(id), {method:'DELETE'}); }catch(e){}
362
  if(id === state.currentSession){
 
374
  }
375
 
376
  function saveSessions(){
377
+ try{ localStorage.setItem('sessions', JSON.stringify(state.sessions)); }
378
+ catch(e){ console.warn('LocalStorage full'); }
 
 
 
379
  }
380
 
381
  async function clearCurrentSession(){
382
+ if(!confirm('Pulire questa chat?')) return;
383
  state.sessions[state.currentSession].messages = [];
384
  try{
385
  await fetch('/v1/memory', {
 
392
  loadCurrentChat();
393
  updateStats();
394
  renderSessions();
395
+ showToast('πŸ—‘οΈ Pulita');
396
  }
397
 
398
  // ═══════════════════════════════════════════════════════
399
+ // CHAT (FIX: rendering separato dal contenuto)
400
  // ═══════════════════════════════════════════════════════
401
  function loadCurrentChat(){
402
  const area = document.getElementById('chatArea');
403
  const msgs = state.sessions[state.currentSession].messages;
404
  area.innerHTML = '';
405
  if(msgs.length === 0){
406
+ area.innerHTML =
407
+ '<div class="welcome">' +
408
+ '<h1>πŸš€ NVIDIA AI Multi-Model</h1>' +
409
+ '<p>Sessione: <b>' + escapeHtml(state.currentSession) + '</b></p>' +
410
+ '<div class="suggestions">' +
411
+ '<div class="suggestion" onclick="sendSuggestion(\'Ciao! Chi sei?\')"><div class="suggestion-icon">πŸ‘‹</div><div class="suggestion-text">Ciao! Chi sei?</div></div>' +
412
+ '<div class="suggestion" onclick="sendSuggestion(\'Scrivi codice Python per il quicksort\')"><div class="suggestion-icon">πŸ’»</div><div class="suggestion-text">Codice Python quicksort</div></div>' +
413
+ '<div class="suggestion" onclick="sendSuggestion(\'Spiegami la meccanica quantistica\')"><div class="suggestion-icon">πŸ”¬</div><div class="suggestion-text">Meccanica quantistica</div></div>' +
414
+ '<div class="suggestion" onclick="sendSuggestion(\'Scrivi una poesia in italiano\')"><div class="suggestion-icon">πŸ“</div><div class="suggestion-text">Scrivi una poesia</div></div>' +
415
+ '</div></div>';
416
  return;
417
  }
418
  msgs.forEach(m => appendMessage(m.role, m.content, false));
 
420
 
421
  function appendMessage(role, content, save=true){
422
  const area = document.getElementById('chatArea');
423
+ const welcome = area.querySelector('.welcome');
424
  if(welcome) welcome.remove();
425
+
426
  const div = document.createElement('div');
427
  div.className = 'message';
428
  const isUser = role === 'user';
429
  const currentModel = document.getElementById('modelSelect').value;
430
  const modelName = state.models[currentModel]?.name || 'Assistente';
431
+
432
+ // FIX: contenuto renderizzato con innerHTML MA solo dopo escape+markdown
433
+ const avatar = document.createElement('div');
434
+ avatar.className = 'msg-avatar ' + role;
435
+ avatar.textContent = isUser ? 'πŸ‘€' : 'πŸ€–';
436
+
437
+ const contentDiv = document.createElement('div');
438
+ contentDiv.className = 'msg-content';
439
+
440
+ const roleDiv = document.createElement('div');
441
+ roleDiv.className = 'msg-role';
442
+ roleDiv.textContent = isUser ? 'Tu' : modelName;
443
+
444
+ const textDiv = document.createElement('div');
445
+ textDiv.className = 'msg-text ' + role;
446
+ textDiv.innerHTML = renderMarkdown(content); // safe: renderMarkdown fa giΓ  escape
447
+
448
+ const actionsDiv = document.createElement('div');
449
+ actionsDiv.className = 'msg-actions';
450
+ const copyBtn = document.createElement('button');
451
+ copyBtn.className = 'msg-btn';
452
+ copyBtn.textContent = 'πŸ“‹ Copia';
453
+ copyBtn.onclick = ()=>{
454
+ navigator.clipboard.writeText(textDiv.innerText);
455
+ showToast('βœ… Copiato');
456
+ };
457
+ actionsDiv.appendChild(copyBtn);
458
+
459
+ contentDiv.appendChild(roleDiv);
460
+ contentDiv.appendChild(textDiv);
461
+ contentDiv.appendChild(actionsDiv);
462
+
463
+ div.appendChild(avatar);
464
+ div.appendChild(contentDiv);
465
  area.appendChild(div);
466
  area.scrollTop = area.scrollHeight;
467
+
468
  if(save){
469
  state.sessions[state.currentSession].messages.push({role, content, timestamp: Date.now()});
470
  saveSessions();
471
  renderSessions();
472
  }
473
+ return textDiv; // ritorna l'elemento DOM per streaming
474
+ }
475
+
476
+ function showTypingIndicator(textDiv){
477
+ textDiv.innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>';
478
  }
479
 
480
  function escapeHtml(text){
481
  const div = document.createElement('div');
482
+ div.textContent = String(text ?? '');
483
  return div.innerHTML;
484
  }
485
 
486
+ function escapeAttr(text){
487
+ return String(text ?? '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
488
+ }
489
+
490
  function renderMarkdown(text){
491
+ if(!text) return '';
492
  text = escapeHtml(text);
493
  text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_,lang,code)=>
494
+ '<pre><code class="lang-' + (lang||'') + '">' + code.trim() + '</code></pre>');
495
  text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>');
496
  text = text.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
497
  text = text.replace(/\*([^*\n]+)\*/g, '<i>$1</i>');
 
499
  return text;
500
  }
501
 
 
 
 
 
 
 
502
  function sendSuggestion(text){
503
  document.getElementById('userInput').value = text;
504
  sendMessage();
 
509
  const message = input.value.trim();
510
  if(!message || state.isGenerating) return;
511
 
 
512
  const modelSel = document.getElementById('modelSelect');
513
  let model = modelSel.value;
514
  if(!model || model.trim() === ''){
515
  model = localStorage.getItem('selectedModel') || state.defaultModel || DEFAULT_MODEL;
516
  modelSel.value = model;
 
517
  }
518
+ console.log('πŸ“€ model:', model, '| session:', state.currentSession);
519
 
520
  const systemPrompt = document.getElementById('systemPrompt').value || 'Sei un assistente utile.';
521
  const temperature = parseFloat(document.getElementById('temperature').value);
 
530
  state.isGenerating = true;
531
  document.getElementById('sendBtn').disabled = true;
532
 
533
+ const textDiv = appendMessage('assistant', '', false);
534
+ showTypingIndicator(textDiv);
535
  let fullText = '';
536
 
537
  try{
 
544
  use_memory: useMemory
545
  }, (chunk)=>{
546
  fullText += chunk;
547
+ textDiv.innerHTML = renderMarkdown(fullText);
548
  document.getElementById('chatArea').scrollTop = document.getElementById('chatArea').scrollHeight;
549
  });
550
+ textDiv.innerHTML = renderMarkdown(fullText);
551
  } else {
552
  const r = await fetch('/v1/chat', {
553
  method:'POST',
 
561
  })
562
  });
563
  const data = await r.json();
564
+ if(!r.ok) throw new Error(data.detail || data.error || 'Errore server');
 
 
565
  fullText = data.response || '';
566
+ textDiv.innerHTML = renderMarkdown(fullText);
567
  }
568
 
569
  if(fullText){
 
575
  updateStats();
576
  }
577
  }catch(e){
578
+ console.error('❌', e);
579
+ textDiv.innerHTML = '<div class="error-msg">❌ ' + escapeHtml(e.message) + '<br><br>πŸ’‘ Prova un altro modello (LLaMA 3.1 8B, Mistral 7B)</div>';
580
  } finally {
581
  state.isGenerating = false;
582
  document.getElementById('sendBtn').disabled = false;
 
586
 
587
  async function streamChat(params, onChunk){
588
  return new Promise((resolve, reject)=>{
 
589
  const model = params.model || localStorage.getItem('selectedModel') || DEFAULT_MODEL;
590
+ console.log('πŸš€ SSE model:', model);
 
591
  const qs = new URLSearchParams({
592
  message: String(params.message || ''),
593
  model: String(model),
 
597
  max_tokens: String(params.max_tokens || 4096),
598
  use_memory: String(params.use_memory !== false)
599
  });
 
600
  const es = new EventSource('/v1/chat/stream?' + qs.toString());
601
  state.currentEventSource = es;
602
+ let received = false;
 
603
  let hadError = false;
604
 
605
+ es.addEventListener('start', (e)=>console.log('▢️', e.data));
 
 
 
606
  es.addEventListener('token', (e)=>{
607
  try{
608
  const d = JSON.parse(e.data);
609
+ if(d.token){ received = true; onChunk(d.token); }
610
+ }catch(err){}
 
 
 
611
  });
 
612
  es.addEventListener('done', (e)=>{
613
+ console.log('βœ… done');
614
  es.close();
615
  state.currentEventSource = null;
616
  resolve();
617
  });
 
618
  es.addEventListener('error', (e)=>{
619
+ console.error('❌ SSE error:', e.data);
620
  hadError = true;
621
  es.close();
622
  state.currentEventSource = null;
 
623
  if(e.data){
624
+ try{ reject(new Error(JSON.parse(e.data).error || 'Stream error')); }
625
+ catch{ reject(new Error('Stream error')); }
626
+ } else if(received){
 
 
 
 
 
627
  resolve();
628
  } else {
629
+ reject(new Error('Connessione SSE fallita per modello ' + model));
630
  }
631
  });
 
 
632
  setTimeout(()=>{
633
+ if(!received && !hadError){
634
  es.close();
635
+ reject(new Error('Timeout 60s'));
636
  }
637
  }, 60000);
638
  });
639
  }
640
 
641
+ // ═══════════════════════════════════════════════════════
642
+ // TEST MODELLO CORRENTE
643
+ // ═══════════════════════════════════════════════════════
644
+ async function testCurrentModel(){
645
+ const model = document.getElementById('modelSelect').value;
646
+ showToast('πŸ§ͺ Test in corso...');
647
+ try{
648
+ const r = await fetch('/v1/debug/test-model?model=' + encodeURIComponent(model));
649
+ const data = await r.json();
650
+ if(data.status === 'βœ… OK'){
651
+ showToast('βœ… ' + model + ' OK (' + data.elapsed_ms + 'ms)');
652
+ } else {
653
+ showToast('❌ ' + model + ' non funziona', true);
654
+ }
655
+ }catch(e){
656
+ showToast('❌ Errore test', true);
657
+ }
658
+ }
659
+
660
  // ═══════════════════════════════════════════════════════
661
  // STATS
662
  // ═══════════════════════════════════════════════════════
 
699
  // ═══════════════════════════════════════════════════════
700
  function exportChat(){
701
  const s = state.sessions[state.currentSession];
702
+ const blob = new Blob([JSON.stringify(s, null, 2)], {type:'application/json'});
 
703
  const url = URL.createObjectURL(blob);
704
  const a = document.createElement('a');
705
  a.href = url;
706
+ a.download = 'chat-' + state.currentSession + '-' + Date.now() + '.json';
707
  a.click();
708
  URL.revokeObjectURL(url);
709
+ showToast('βœ… Esportata');
710
  }
711
 
712
  // ═══════════════════════════════════════════════════════
 
716
  document.getElementById('modal').classList.add('active');
717
  const title = document.getElementById('modalTitle');
718
  const body = document.getElementById('modalBody');
719
+ const base = location.origin;
720
 
721
  if(type === 'models'){
722
+ title.textContent = 'πŸ€– Catalogo Modelli';
723
+ let html = '<p style="color:var(--text-dim);margin-bottom:12px">Totale: <b>' + Object.keys(state.models).length + '</b></p>';
724
+ html += '<table class="endpoint-table"><tr><th>Cat</th><th>Nome</th><th>ID</th><th>Ctx</th></tr>';
725
  for(const [id,m] of Object.entries(state.models)){
726
+ html += '<tr><td>' + escapeHtml(m.category) + '</td><td>' + escapeHtml(m.name) + '</td><td><code>' + escapeHtml(id) + '</code></td><td>' + m.context.toLocaleString() + '</td></tr>';
727
  }
728
  html += '</table>';
729
  body.innerHTML = html;
730
+ }
731
+ else if(type === 'test'){
732
+ title.textContent = 'πŸš€ Test Parallelo Async';
733
+ body.innerHTML =
734
+ '<p>Testa <b>tutti i modelli in parallelo</b> con 15 richieste simultanee.<br>' +
735
+ 'Impiega solo ~5-10 secondi invece di 60+ seriali.</p><br>' +
736
+ '<button class="btn-new" onclick="runParallelTest()">πŸš€ Avvia Test Parallelo</button> ' +
737
+ '<button class="btn-new" onclick="runAutoClean()">🧹 Auto-Clean Catalogo</button>' +
738
+ '<div id="testResults" style="margin-top:16px"></div>';
739
+ }
740
+ else if(type === 'api'){
741
  title.textContent = 'πŸ“– API Reference';
742
+ body.innerHTML =
743
+ '<h3>Chat GET</h3><div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl "' + base + '/v1/chat/get?message=Ciao&model=meta/llama-3.1-8b-instruct&session_id=test"</div>' +
744
+ '<h3>Chat POST</h3><div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ' + base + '/v1/chat \\\n -H "Content-Type: application/json" \\\n -d \'{"message":"Ciao!","model":"meta/llama-3.1-8b-instruct"}\'</div>' +
745
+ '<h3>SSE Streaming</h3><div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -N "' + base + '/v1/chat/stream?message=Ciao&model=meta/llama-3.1-8b-instruct"</div>' +
746
+ '<h3>πŸš€ Test Parallelo Ultra-veloce</h3><div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl "' + base + '/v1/debug/test-all-parallel?concurrency=15"</div>' +
747
+ '<h3>🧹 Auto-Clean Catalogo</h3><div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl "' + base + '/v1/debug/auto-clean"</div>';
748
+ }
749
+ else if(type === 'mcp'){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  title.textContent = 'πŸ”Œ MCP Server';
751
+ body.innerHTML =
752
+ '<h3>Config Claude Desktop / Cursor</h3>' +
753
+ '<div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>{\n "mcpServers": {\n "nvidia-ai": {\n "url": "' + base + '/v1/mcp",\n "transport": "http"\n }\n }\n}</div>' +
754
+ '<h3>Tools disponibili</h3>' +
755
+ '<ul style="line-height:1.8">' +
756
+ '<li><code>nvidia_chat</code> β€” Chat con memoria</li>' +
757
+ '<li><code>multi_model_compare</code> β€” πŸ”₯ Confronta piΓΉ modelli in PARALLELO</li>' +
758
+ '<li><code>list_models</code> β€” Lista modelli</li>' +
759
+ '<li><code>memory_stats</code> β€” Statistiche memoria</li>' +
760
+ '<li><code>memory_clear</code> β€” Pulisci memoria</li>' +
761
+ '<li><code>get_conversation_history</code> β€” Cronologia</li>' +
762
+ '</ul>' +
763
+ '<h3>Test</h3>' +
764
+ '<div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ' + base + '/v1/mcp \\\n -H "Content-Type: application/json" \\\n -d \'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}\'</div>';
765
  }
766
+ }
767
+
768
+ async function runParallelTest(){
769
+ const results = document.getElementById('testResults');
770
+ results.innerHTML = '<div class="test-loading"><div class="typing-dots"><span></span><span></span><span></span></div><p>Test in corso... (~5-10 sec)</p></div>';
771
+ try{
772
+ const r = await fetch('/v1/debug/test-all-parallel?concurrency=15');
773
+ const data = await r.json();
774
+ let html = '<h3>⏱️ Completato in ' + data.summary.total_time_seconds + 's</h3>';
775
+ html += '<p>βœ… Funzionanti: <b>' + data.summary.working + '</b> / ' + data.summary.total_tested + '</p>';
776
+ html += '<p>❌ Rotti: <b>' + data.summary.broken + '</b></p>';
777
+ html += '<h4>βœ… Modelli funzionanti (ordinati per velocitΓ ):</h4>';
778
+ html += '<table class="endpoint-table"><tr><th>Model</th><th>ms</th></tr>';
779
+ for(const w of data.working_models_by_speed){
780
+ html += '<tr><td><code>' + escapeHtml(w.model) + '</code></td><td>' + w.elapsed_ms + '</td></tr>';
781
+ }
782
+ html += '</table>';
783
+ if(data.broken_models.length > 0){
784
+ html += '<h4>❌ Modelli non funzionanti:</h4>';
785
+ html += '<div class="code-block">' + data.broken_ids_only.map(escapeHtml).join('\n') + '</div>';
786
+ }
787
+ results.innerHTML = html;
788
+ showToast('βœ… Test completato in ' + data.summary.total_time_seconds + 's');
789
+ }catch(e){
790
+ results.innerHTML = '<div class="error-msg">❌ ' + escapeHtml(e.message) + '</div>';
791
  }
792
  }
793
 
794
+ async function runAutoClean(){
795
+ const results = document.getElementById('testResults');
796
+ results.innerHTML = '<div class="test-loading"><div class="typing-dots"><span></span><span></span><span></span></div><p>Auto-clean in corso...</p></div>';
797
+ try{
798
+ const r = await fetch('/v1/debug/auto-clean');
799
+ const data = await r.json();
800
+ let html = '<h3>🧹 Catalogo Pulito</h3>';
801
+ html += '<p>Da <b>' + data.summary.original_count + '</b> a <b>' + data.summary.clean_catalog_size + '</b> modelli in ' + data.summary.total_time_seconds + 's</p>';
802
+ html += '<h4>Snippet Python da copiare in app.py:</h4>';
803
+ html += '<div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>' + escapeHtml(data.python_snippet) + '</div>';
804
+ results.innerHTML = html;
805
+ showToast('βœ… Auto-clean completato');
806
+ }catch(e){
807
+ results.innerHTML = '<div class="error-msg">❌ ' + escapeHtml(e.message) + '</div>';
808
+ }
809
  }
810
 
811
  function copyBlock(btn){
812
+ const text = btn.parentElement.innerText.replace(/^Copia\s*/,'').trim();
813
  navigator.clipboard.writeText(text);
814
  btn.textContent = 'βœ…';
815
  setTimeout(()=>btn.textContent='Copia', 1500);
 
823
  if(e.target.id === 'modal') closeModal();
824
  });
825
 
 
 
 
826
  function showToast(msg, isError=false){
827
  const t = document.getElementById('toast');
828
  t.textContent = msg;
 
830
  setTimeout(()=>t.classList.remove('show'), 3000);
831
  }
832
 
 
833
  init();
834
  </script>
835
  </body>