Update static/index.html
Browse files- 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 |
-
|
| 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-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
.
|
| 143 |
-
.
|
| 144 |
-
.
|
| 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
|
| 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
|
| 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
|
| 364 |
}catch(e){
|
| 365 |
-
console.error('β
|
| 366 |
-
showToast('β Errore
|
| 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>
|
| 376 |
-
<b>Categoria:</b>
|
| 377 |
-
<b>Context:</b>
|
| 378 |
-
<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">π¬
|
| 401 |
-
<span class="session-badge">
|
| 402 |
-
<button class="session-delete"
|
| 403 |
-
|
| 404 |
-
|
|
|
|
|
|
|
|
|
|
| 405 |
list.appendChild(div);
|
| 406 |
}
|
| 407 |
}
|
|
@@ -427,7 +356,7 @@ function switchSession(id){
|
|
| 427 |
}
|
| 428 |
|
| 429 |
async function deleteSession(id){
|
| 430 |
-
if(!confirm(
|
| 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 |
-
|
| 450 |
-
}catch(e){
|
| 451 |
-
console.warn('LocalStorage pieno, pulizia messaggi vecchi');
|
| 452 |
-
}
|
| 453 |
}
|
| 454 |
|
| 455 |
async function clearCurrentSession(){
|
| 456 |
-
if(!confirm('Pulire
|
| 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('ποΈ
|
| 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 =
|
| 481 |
-
<
|
| 482 |
-
<
|
| 483 |
-
<
|
| 484 |
-
|
| 485 |
-
<div class="suggestion" onclick="sendSuggestion('
|
| 486 |
-
<div class="suggestion" onclick="sendSuggestion('
|
| 487 |
-
<div class="suggestion" onclick="sendSuggestion('
|
| 488 |
-
|
| 489 |
-
|
| 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 =
|
| 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 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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('π€
|
| 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
|
|
|
|
| 580 |
let fullText = '';
|
| 581 |
|
| 582 |
try{
|
|
@@ -589,10 +544,10 @@ async function sendMessage(){
|
|
| 589 |
use_memory: useMemory
|
| 590 |
}, (chunk)=>{
|
| 591 |
fullText += chunk;
|
| 592 |
-
|
| 593 |
document.getElementById('chatArea').scrollTop = document.getElementById('chatArea').scrollHeight;
|
| 594 |
});
|
| 595 |
-
|
| 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 |
-
|
| 614 |
}
|
| 615 |
|
| 616 |
if(fullText){
|
|
@@ -622,8 +575,8 @@ async function sendMessage(){
|
|
| 622 |
updateStats();
|
| 623 |
}
|
| 624 |
}catch(e){
|
| 625 |
-
console.error('β
|
| 626 |
-
|
| 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
|
| 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 |
-
|
| 665 |
-
onChunk(d.token);
|
| 666 |
-
}
|
| 667 |
-
}catch(err){ console.error('token parse error', err); }
|
| 668 |
});
|
| 669 |
-
|
| 670 |
es.addEventListener('done', (e)=>{
|
| 671 |
-
console.log('β
|
| 672 |
es.close();
|
| 673 |
state.currentEventSource = null;
|
| 674 |
resolve();
|
| 675 |
});
|
| 676 |
-
|
| 677 |
es.addEventListener('error', (e)=>{
|
| 678 |
-
console.error('β SSE error
|
| 679 |
hadError = true;
|
| 680 |
es.close();
|
| 681 |
state.currentEventSource = null;
|
| 682 |
-
|
| 683 |
if(e.data){
|
| 684 |
-
try{
|
| 685 |
-
|
| 686 |
-
|
| 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
|
| 695 |
}
|
| 696 |
});
|
| 697 |
-
|
| 698 |
-
// Safety timeout
|
| 699 |
setTimeout(()=>{
|
| 700 |
-
if(!
|
| 701 |
es.close();
|
| 702 |
-
reject(new Error('Timeout
|
| 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
|
| 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 =
|
| 756 |
a.click();
|
| 757 |
URL.revokeObjectURL(url);
|
| 758 |
-
showToast('β
|
| 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
|
| 771 |
-
let html =
|
| 772 |
-
html += '<table class="endpoint-table"><tr><th>
|
| 773 |
for(const [id,m] of Object.entries(state.models)){
|
| 774 |
-
html +=
|
| 775 |
}
|
| 776 |
html += '</table>';
|
| 777 |
body.innerHTML = html;
|
| 778 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
title.textContent = 'π API Reference';
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
<div class="
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 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 |
-
|
| 850 |
-
|
| 851 |
-
<
|
| 852 |
-
<
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
}
|
| 859 |
-
}
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 876 |
}
|
| 877 |
}
|
| 878 |
|
| 879 |
-
function
|
| 880 |
-
document.
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
}
|
| 885 |
|
| 886 |
function copyBlock(btn){
|
| 887 |
-
const text = btn.parentElement.innerText.replace(
|
| 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, '"').replace(/'/g, ''');
|
| 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>
|