elmarcito commited on
Commit
805fab4
Β·
verified Β·
1 Parent(s): 3cbc846

Upload static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +914 -0
static/index.html ADDED
@@ -0,0 +1,914 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="it">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
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
+
32
+ /* ── Header ── */
33
+ header{
34
+ grid-area:header;
35
+ background:linear-gradient(90deg,#0a0e14,#141a24);
36
+ border-bottom:1px solid var(--border);
37
+ display:flex;align-items:center;padding:0 20px;gap:20px;
38
+ z-index:10;
39
+ }
40
+ .logo{display:flex;align-items:center;gap:10px;font-weight:700;font-size:18px}
41
+ .logo-badge{background:var(--nvidia);color:#000;padding:3px 8px;border-radius:4px;font-size:11px;font-weight:800}
42
+ .nav-tabs{display:flex;gap:5px;margin-left:20px}
43
+ .nav-tab{padding:8px 16px;border-radius:8px;cursor:pointer;color:var(--text-dim);font-size:14px;transition:all .2s}
44
+ .nav-tab:hover{background:var(--card);color:var(--text)}
45
+ .nav-tab.active{background:var(--accent-glow);color:var(--accent)}
46
+ .header-right{margin-left:auto;display:flex;gap:12px;align-items:center}
47
+ .status{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-dim)}
48
+ .status-dot{width:8px;height:8px;border-radius:50%;background:var(--nvidia);box-shadow:0 0 10px var(--nvidia);animation:pulse 2s infinite}
49
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
50
+ .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}
51
+ .btn-icon:hover{border-color:var(--accent);color:var(--accent)}
52
+
53
+ /* ── Sidebar (sessioni) ── */
54
+ .sidebar{
55
+ grid-area:sidebar;
56
+ background:var(--card);
57
+ border-right:1px solid var(--border);
58
+ display:flex;flex-direction:column;overflow:hidden;
59
+ }
60
+ .sidebar-header{padding:16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
61
+ .sidebar-title{font-size:12px;text-transform:uppercase;color:var(--text-dim);letter-spacing:1px}
62
+ .btn-new{background:var(--accent);color:#000;border:none;padding:6px 12px;border-radius:6px;cursor:pointer;font-weight:600;font-size:12px;transition:all .2s}
63
+ .btn-new:hover{background:#8ad100;transform:translateY(-1px)}
64
+ .session-list{flex:1;overflow-y:auto;padding:8px}
65
+ .session-item{
66
+ padding:10px 12px;border-radius:8px;cursor:pointer;margin-bottom:4px;
67
+ display:flex;justify-content:space-between;align-items:center;
68
+ transition:all .15s;font-size:13px;
69
+ }
70
+ .session-item:hover{background:var(--card-hover)}
71
+ .session-item.active{background:var(--accent-glow);color:var(--accent);border-left:3px solid var(--accent)}
72
+ .session-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
73
+ .session-badge{background:var(--border);color:var(--text-dim);padding:2px 6px;border-radius:10px;font-size:10px}
74
+ .session-delete{opacity:0;background:none;border:none;color:var(--error);cursor:pointer;font-size:14px;padding:0 4px}
75
+ .session-item:hover .session-delete{opacity:1}
76
+
77
+ /* ── Main (chat area) ── */
78
+ .main{grid-area:main;display:flex;flex-direction:column;overflow:hidden}
79
+ .chat-area{flex:1;overflow-y:auto;padding:24px 32px;scroll-behavior:smooth}
80
+ .welcome{text-align:center;padding:60px 20px;color:var(--text-dim)}
81
+ .welcome h1{font-size:32px;background:linear-gradient(135deg,var(--nvidia),#00d4ff);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:12px}
82
+ .welcome p{margin-bottom:24px;font-size:15px}
83
+ .suggestions{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;max-width:700px;margin:0 auto}
84
+ .suggestion{background:var(--card);border:1px solid var(--border);padding:16px;border-radius:12px;cursor:pointer;transition:all .2s;text-align:left}
85
+ .suggestion:hover{border-color:var(--accent);background:var(--card-hover);transform:translateY(-2px)}
86
+ .suggestion-icon{font-size:24px;margin-bottom:8px}
87
+ .suggestion-text{font-size:13px;color:var(--text)}
88
+
89
+ /* Messages */
90
+ .message{margin-bottom:20px;display:flex;gap:12px;animation:fadeIn .3s}
91
+ @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
92
+ .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}
93
+ .msg-avatar.user{background:linear-gradient(135deg,#3b82f6,#1e40af);color:#fff}
94
+ .msg-avatar.assistant{background:linear-gradient(135deg,var(--nvidia),#4a7c00);color:#000}
95
+ .msg-content{flex:1;min-width:0}
96
+ .msg-role{font-weight:600;font-size:13px;margin-bottom:4px;color:var(--text-dim)}
97
+ .msg-text{background:var(--card);padding:14px 16px;border-radius:12px;border:1px solid var(--border);line-height:1.6;word-wrap:break-word;overflow-wrap:break-word}
98
+ .msg-text.user{background:transparent;border:none;padding:14px 0}
99
+ .msg-text pre{background:var(--code);border:1px solid var(--border);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0;font-family:'Fira Code','Courier New',monospace;font-size:13px}
100
+ .msg-text code{background:var(--code);padding:2px 6px;border-radius:4px;font-family:'Fira Code',monospace;font-size:13px}
101
+ .msg-text pre code{background:none;padding:0}
102
+ .msg-actions{display:flex;gap:8px;margin-top:6px;opacity:0;transition:opacity .2s}
103
+ .message:hover .msg-actions{opacity:1}
104
+ .msg-btn{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:12px;padding:2px 6px;border-radius:4px}
105
+ .msg-btn:hover{color:var(--accent);background:var(--card)}
106
+ .typing{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent);animation:typing 1s infinite}
107
+ @keyframes typing{0%,100%{opacity:.3}50%{opacity:1}}
108
+
109
+ /* Input area */
110
+ .input-area{
111
+ padding:16px 32px 20px;
112
+ border-top:1px solid var(--border);
113
+ background:var(--bg);
114
+ }
115
+ .input-wrapper{
116
+ display:flex;gap:8px;align-items:flex-end;
117
+ background:var(--card);border:1px solid var(--border);border-radius:16px;
118
+ padding:8px;transition:border-color .2s;
119
+ }
120
+ .input-wrapper:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
121
+ #userInput{
122
+ flex:1;background:none;border:none;color:var(--text);
123
+ padding:10px;font-size:15px;font-family:inherit;
124
+ resize:none;max-height:200px;min-height:24px;outline:none;
125
+ }
126
+ .send-btn{
127
+ background:var(--accent);color:#000;border:none;
128
+ width:40px;height:40px;border-radius:10px;cursor:pointer;
129
+ display:flex;align-items:center;justify-content:center;font-size:18px;
130
+ transition:all .2s;flex-shrink:0;
131
+ }
132
+ .send-btn:hover:not(:disabled){background:#8ad100;transform:scale(1.05)}
133
+ .send-btn:disabled{background:var(--border);color:var(--text-dim);cursor:not-allowed}
134
+ .input-footer{display:flex;justify-content:space-between;margin-top:8px;font-size:11px;color:var(--text-dim);padding:0 8px}
135
+ .stream-toggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}
136
+ .stream-toggle input{accent-color:var(--accent)}
137
+
138
+ /* ── Config panel ── */
139
+ .config{
140
+ grid-area:config;background:var(--card);
141
+ border-left:1px solid var(--border);
142
+ overflow-y:auto;padding:20px;
143
+ }
144
+ .config-section{margin-bottom:24px}
145
+ .config-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-dim);margin-bottom:8px;display:block;font-weight:600}
146
+ .config-select,.config-input,.config-textarea{
147
+ width:100%;background:var(--bg);border:1px solid var(--border);color:var(--text);
148
+ padding:8px 10px;border-radius:6px;font-size:13px;font-family:inherit;
149
+ transition:border-color .2s;
150
+ }
151
+ .config-select:focus,.config-input:focus,.config-textarea:focus{outline:none;border-color:var(--accent)}
152
+ .config-textarea{resize:vertical;min-height:80px;font-family:inherit}
153
+ .slider-row{display:flex;align-items:center;gap:10px}
154
+ .slider-row input[type=range]{flex:1;accent-color:var(--accent)}
155
+ .slider-value{background:var(--bg);padding:4px 8px;border-radius:4px;font-size:12px;font-family:monospace;min-width:50px;text-align:center;color:var(--accent)}
156
+ .model-info{background:var(--bg);padding:10px;border-radius:6px;font-size:11px;color:var(--text-dim);margin-top:6px;line-height:1.5}
157
+ .model-info b{color:var(--text)}
158
+ .checkbox-row{display:flex;align-items:center;gap:8px;padding:8px;background:var(--bg);border-radius:6px;cursor:pointer}
159
+ .checkbox-row input{accent-color:var(--accent)}
160
+ .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
161
+ .stat{background:var(--bg);padding:10px;border-radius:6px;text-align:center}
162
+ .stat-val{font-size:18px;font-weight:700;color:var(--accent)}
163
+ .stat-lbl{font-size:10px;color:var(--text-dim);text-transform:uppercase;margin-top:2px}
164
+
165
+ /* ── Modal (API playground) ── */
166
+ .modal-overlay{
167
+ position:fixed;inset:0;background:rgba(0,0,0,.7);
168
+ display:none;align-items:center;justify-content:center;z-index:100;padding:20px;
169
+ }
170
+ .modal-overlay.active{display:flex}
171
+ .modal{
172
+ background:var(--card);border-radius:16px;max-width:900px;width:100%;
173
+ max-height:85vh;overflow:hidden;display:flex;flex-direction:column;border:1px solid var(--border);
174
+ }
175
+ .modal-header{padding:20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
176
+ .modal-title{font-size:18px;font-weight:700}
177
+ .modal-close{background:none;border:none;color:var(--text);font-size:24px;cursor:pointer}
178
+ .modal-body{padding:20px;overflow-y:auto;flex:1}
179
+ .tab-bar{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:16px}
180
+ .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}
181
+ .tab-btn.active{color:var(--accent);border-color:var(--accent)}
182
+ .tab-panel{display:none}
183
+ .tab-panel.active{display:block}
184
+ .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}
185
+ .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}
186
+ .copy-btn:hover{background:var(--accent);color:#000}
187
+ .endpoint-table{width:100%;border-collapse:collapse;margin:8px 0;font-size:13px}
188
+ .endpoint-table th,.endpoint-table td{padding:8px 10px;border:1px solid var(--border);text-align:left}
189
+ .endpoint-table th{background:var(--code);color:var(--accent)}
190
+ .method-badge{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700}
191
+ .method-get{background:#1976d2;color:#fff}
192
+ .method-post{background:var(--accent);color:#000}
193
+
194
+ /* Toast */
195
+ .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}
196
+ .toast.show{display:block}
197
+ @keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
198
+
199
+ /* Scrollbar */
200
+ ::-webkit-scrollbar{width:8px;height:8px}
201
+ ::-webkit-scrollbar-track{background:var(--bg)}
202
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
203
+ ::-webkit-scrollbar-thumb:hover{background:var(--accent)}
204
+
205
+ /* Responsive */
206
+ @media(max-width:1100px){
207
+ body{grid-template-columns:0 1fr 0}
208
+ .sidebar,.config{display:none}
209
+ }
210
+ </style>
211
+ </head>
212
+ <body>
213
+
214
+ <!-- HEADER -->
215
+ <header>
216
+ <div class="logo">
217
+ <span>πŸš€</span>
218
+ <span>NVIDIA AI</span>
219
+ <span class="logo-badge">NIM</span>
220
+ </div>
221
+ <div class="nav-tabs">
222
+ <div class="nav-tab active" data-view="chat">πŸ’¬ Chat</div>
223
+ <div class="nav-tab" onclick="openModal('models')">πŸ€– Modelli</div>
224
+ <div class="nav-tab" onclick="openModal('api')">πŸ“– API</div>
225
+ <div class="nav-tab" onclick="openModal('mcp')">πŸ”Œ MCP</div>
226
+ <a class="nav-tab" href="/ui" target="_blank" style="text-decoration:none;color:inherit">🎨 Gradio</a>
227
+ <a class="nav-tab" href="/docs" target="_blank" style="text-decoration:none;color:inherit">πŸ“š Swagger</a>
228
+ </div>
229
+ <div class="header-right">
230
+ <div class="status"><div class="status-dot"></div><span id="statusText">Connesso</span></div>
231
+ </div>
232
+ </header>
233
+
234
+ <!-- SIDEBAR SESSIONI -->
235
+ <aside class="sidebar">
236
+ <div class="sidebar-header">
237
+ <span class="sidebar-title">Sessioni</span>
238
+ <button class="btn-new" onclick="newSession()">+ Nuova</button>
239
+ </div>
240
+ <div class="session-list" id="sessionList"></div>
241
+ </aside>
242
+
243
+ <!-- MAIN CHAT -->
244
+ <main class="main">
245
+ <div class="chat-area" id="chatArea">
246
+ <div class="welcome" id="welcome">
247
+ <h1>πŸš€ NVIDIA AI Multi-Model</h1>
248
+ <p>Chatta con 35+ modelli NVIDIA NIM β€’ Memoria persistente β€’ SSE Streaming</p>
249
+ <div class="suggestions">
250
+ <div class="suggestion" onclick="sendSuggestion('Spiegami la meccanica quantistica in modo semplice')">
251
+ <div class="suggestion-icon">πŸ”¬</div>
252
+ <div class="suggestion-text">Spiegami la meccanica quantistica in modo semplice</div>
253
+ </div>
254
+ <div class="suggestion" onclick="sendSuggestion('Scrivi una funzione Python per il quicksort')">
255
+ <div class="suggestion-icon">πŸ’»</div>
256
+ <div class="suggestion-text">Scrivi una funzione Python per il quicksort</div>
257
+ </div>
258
+ <div class="suggestion" onclick="sendSuggestion('Raccontami una storia breve sci-fi in italiano')">
259
+ <div class="suggestion-icon">πŸ“–</div>
260
+ <div class="suggestion-text">Raccontami una storia breve sci-fi in italiano</div>
261
+ </div>
262
+ <div class="suggestion" onclick="sendSuggestion('Confronta React vs Vue vs Svelte')">
263
+ <div class="suggestion-icon">⚑</div>
264
+ <div class="suggestion-text">Confronta React vs Vue vs Svelte</div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+
270
+ <div class="input-area">
271
+ <div class="input-wrapper">
272
+ <textarea id="userInput" placeholder="Scrivi un messaggio... (Shift+Enter per nuova riga)" rows="1"></textarea>
273
+ <button class="send-btn" id="sendBtn" onclick="sendMessage()">➀</button>
274
+ </div>
275
+ <div class="input-footer">
276
+ <label class="stream-toggle">
277
+ <input type="checkbox" id="streamMode" checked>
278
+ <span>πŸ“‘ Streaming SSE</span>
279
+ </label>
280
+ <span id="tokenCount">0 caratteri</span>
281
+ </div>
282
+ </div>
283
+ </main>
284
+
285
+ <!-- CONFIG PANEL -->
286
+ <aside class="config">
287
+ <div class="config-section">
288
+ <label class="config-label">πŸ€– Modello</label>
289
+ <select class="config-select" id="modelSelect"></select>
290
+ <div class="model-info" id="modelInfo"></div>
291
+ </div>
292
+
293
+ <div class="config-section">
294
+ <label class="config-label">πŸ“ System Prompt</label>
295
+ <textarea class="config-textarea" id="systemPrompt">Sei un assistente AI avanzato, preciso e creativo. Rispondi in italiano se l'utente scrive in italiano.</textarea>
296
+ </div>
297
+
298
+ <div class="config-section">
299
+ <label class="config-label">🌑️ Temperature</label>
300
+ <div class="slider-row">
301
+ <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7" oninput="temperatureVal.textContent=this.value">
302
+ <span class="slider-value" id="temperatureVal">0.7</span>
303
+ </div>
304
+ </div>
305
+
306
+ <div class="config-section">
307
+ <label class="config-label">πŸ“ Max Tokens</label>
308
+ <div class="slider-row">
309
+ <input type="range" id="maxTokens" min="256" max="16384" step="256" value="4096" oninput="maxTokensVal.textContent=this.value">
310
+ <span class="slider-value" id="maxTokensVal">4096</span>
311
+ </div>
312
+ </div>
313
+
314
+ <div class="config-section">
315
+ <label class="config-label">🧠 Memoria</label>
316
+ <label class="checkbox-row">
317
+ <input type="checkbox" id="useMemory" checked>
318
+ <span>Attiva memoria breve/lunga</span>
319
+ </label>
320
+ </div>
321
+
322
+ <div class="config-section">
323
+ <label class="config-label">πŸ“Š Statistiche Sessione</label>
324
+ <div class="stats-grid">
325
+ <div class="stat"><div class="stat-val" id="statShort">0</div><div class="stat-lbl">Breve</div></div>
326
+ <div class="stat"><div class="stat-val" id="statLong">0</div><div class="stat-lbl">Lunga</div></div>
327
+ <div class="stat"><div class="stat-val" id="statTokens">0</div><div class="stat-lbl">Token</div></div>
328
+ <div class="stat"><div class="stat-val" id="statTotal">0</div><div class="stat-lbl">Totali</div></div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="config-section">
333
+ <button class="btn-new" style="width:100%;margin-bottom:8px" onclick="clearCurrentSession()">πŸ—‘οΈ Pulisci Chat</button>
334
+ <button class="btn-icon" style="width:100%;justify-content:center" onclick="exportChat()">πŸ’Ύ Esporta Conversazione</button>
335
+ </div>
336
+ </aside>
337
+
338
+ <!-- MODAL -->
339
+ <div class="modal-overlay" id="modal">
340
+ <div class="modal">
341
+ <div class="modal-header">
342
+ <div class="modal-title" id="modalTitle">Info</div>
343
+ <button class="modal-close" onclick="closeModal()">Γ—</button>
344
+ </div>
345
+ <div class="modal-body" id="modalBody"></div>
346
+ </div>
347
+ </div>
348
+
349
+ <div class="toast" id="toast"></div>
350
+
351
+ <script>
352
+ // ═══════════════════════════════════════════════════════
353
+ // STATE
354
+ // ═══════════════════════════════════════════════════════
355
+ const state = {
356
+ currentSession: localStorage.getItem('currentSession') || 'default',
357
+ sessions: JSON.parse(localStorage.getItem('sessions') || '{}'),
358
+ models: {},
359
+ isGenerating: false,
360
+ currentEventSource: null,
361
+ };
362
+
363
+ // ═══════════════════════════════════════════════════════
364
+ // INIT
365
+ // ═══════════════════════════════════════════════════════
366
+ async function init(){
367
+ await loadModels();
368
+ loadSessions();
369
+ loadCurrentChat();
370
+ await updateStats();
371
+ setupInputAutoResize();
372
+ }
373
+
374
+ async function loadModels(){
375
+ try{
376
+ const r = await fetch('/v1/models');
377
+ const data = await r.json();
378
+ state.models = data.models;
379
+ const sel = document.getElementById('modelSelect');
380
+ sel.innerHTML = '';
381
+ // Raggruppa per categoria
382
+ const grouped = {};
383
+ for(const [id,info] of Object.entries(data.models)){
384
+ const cat = info.category;
385
+ if(!grouped[cat]) grouped[cat] = [];
386
+ grouped[cat].push({id, ...info});
387
+ }
388
+ for(const cat of Object.keys(grouped).sort()){
389
+ const og = document.createElement('optgroup');
390
+ og.label = cat;
391
+ for(const m of grouped[cat]){
392
+ const o = document.createElement('option');
393
+ o.value = m.id;
394
+ o.textContent = m.name;
395
+ og.appendChild(o);
396
+ }
397
+ sel.appendChild(og);
398
+ }
399
+ sel.value = localStorage.getItem('selectedModel') || 'meta/llama-3.1-405b-instruct';
400
+ sel.addEventListener('change', ()=>{
401
+ localStorage.setItem('selectedModel', sel.value);
402
+ updateModelInfo();
403
+ });
404
+ updateModelInfo();
405
+ }catch(e){
406
+ console.error('Errore caricamento modelli:', e);
407
+ showToast('❌ Errore caricamento modelli');
408
+ }
409
+ }
410
+
411
+ function updateModelInfo(){
412
+ const id = document.getElementById('modelSelect').value;
413
+ const m = state.models[id];
414
+ if(!m) return;
415
+ document.getElementById('modelInfo').innerHTML = `
416
+ <b>Categoria:</b> ${m.category}<br>
417
+ <b>Context:</b> ${m.context.toLocaleString()} token<br>
418
+ <b>${m.description}</b>
419
+ `;
420
+ }
421
+
422
+ // ═══════════════════════════════════════════════════════
423
+ // SESSIONI
424
+ // ═══════════════════════════════════════════════════════
425
+ function loadSessions(){
426
+ if(!state.sessions[state.currentSession]){
427
+ state.sessions[state.currentSession] = {name: state.currentSession, messages: []};
428
+ saveSessions();
429
+ }
430
+ renderSessions();
431
+ }
432
+
433
+ function renderSessions(){
434
+ const list = document.getElementById('sessionList');
435
+ list.innerHTML = '';
436
+ for(const [id, s] of Object.entries(state.sessions)){
437
+ const div = document.createElement('div');
438
+ div.className = 'session-item' + (id === state.currentSession ? ' active' : '');
439
+ div.innerHTML = `
440
+ <span class="session-name">πŸ’¬ ${s.name}</span>
441
+ <span class="session-badge">${s.messages.length}</span>
442
+ <button class="session-delete" onclick="event.stopPropagation();deleteSession('${id}')">πŸ—‘οΈ</button>
443
+ `;
444
+ div.onclick = ()=>switchSession(id);
445
+ list.appendChild(div);
446
+ }
447
+ }
448
+
449
+ function newSession(){
450
+ const name = prompt('Nome sessione:', 'chat-' + Date.now().toString(36).slice(-5));
451
+ if(!name) return;
452
+ state.sessions[name] = {name, messages: []};
453
+ state.currentSession = name;
454
+ saveSessions();
455
+ localStorage.setItem('currentSession', name);
456
+ renderSessions();
457
+ loadCurrentChat();
458
+ updateStats();
459
+ }
460
+
461
+ function switchSession(id){
462
+ state.currentSession = id;
463
+ localStorage.setItem('currentSession', id);
464
+ renderSessions();
465
+ loadCurrentChat();
466
+ updateStats();
467
+ }
468
+
469
+ async function deleteSession(id){
470
+ if(!confirm(`Eliminare sessione "${id}"?`)) return;
471
+ delete state.sessions[id];
472
+ try{ await fetch('/v1/sessions/' + encodeURIComponent(id), {method:'DELETE'}); }catch(e){}
473
+ if(id === state.currentSession){
474
+ const remaining = Object.keys(state.sessions);
475
+ state.currentSession = remaining[0] || 'default';
476
+ if(!state.sessions[state.currentSession]){
477
+ state.sessions[state.currentSession] = {name: state.currentSession, messages: []};
478
+ }
479
+ localStorage.setItem('currentSession', state.currentSession);
480
+ }
481
+ saveSessions();
482
+ renderSessions();
483
+ loadCurrentChat();
484
+ updateStats();
485
+ }
486
+
487
+ function saveSessions(){
488
+ localStorage.setItem('sessions', JSON.stringify(state.sessions));
489
+ }
490
+
491
+ async function clearCurrentSession(){
492
+ if(!confirm('Pulire i messaggi di questa sessione?')) return;
493
+ state.sessions[state.currentSession].messages = [];
494
+ try{
495
+ await fetch('/v1/memory', {
496
+ method:'POST',
497
+ headers:{'Content-Type':'application/json'},
498
+ body: JSON.stringify({session_id: state.currentSession, action:'clear_all'})
499
+ });
500
+ }catch(e){}
501
+ saveSessions();
502
+ loadCurrentChat();
503
+ updateStats();
504
+ renderSessions();
505
+ }
506
+
507
+ // ═══════════════════════════════════════════════════════
508
+ // CHAT
509
+ // ═══════════════════════════════════════════════════════
510
+ function loadCurrentChat(){
511
+ const area = document.getElementById('chatArea');
512
+ const msgs = state.sessions[state.currentSession].messages;
513
+ if(msgs.length === 0){
514
+ area.innerHTML = document.getElementById('welcome')?.outerHTML || '';
515
+ // Reinserisci welcome se rimosso
516
+ if(!document.getElementById('welcome')){
517
+ area.innerHTML = `<div class="welcome" id="welcome">
518
+ <h1>πŸš€ NVIDIA AI Multi-Model</h1>
519
+ <p>Sessione: <b>${state.currentSession}</b></p>
520
+ <div class="suggestions">
521
+ <div class="suggestion" onclick="sendSuggestion('Ciao! Chi sei?')"><div class="suggestion-icon">πŸ‘‹</div><div class="suggestion-text">Ciao! Chi sei?</div></div>
522
+ <div class="suggestion" onclick="sendSuggestion('Aiutami a scrivere codice Python')"><div class="suggestion-icon">πŸ’»</div><div class="suggestion-text">Aiutami a scrivere codice Python</div></div>
523
+ </div>
524
+ </div>`;
525
+ }
526
+ return;
527
+ }
528
+ area.innerHTML = '';
529
+ msgs.forEach(m => appendMessage(m.role, m.content, false));
530
+ }
531
+
532
+ function appendMessage(role, content, save=true){
533
+ const area = document.getElementById('chatArea');
534
+ const welcome = document.getElementById('welcome');
535
+ if(welcome) welcome.remove();
536
+ const div = document.createElement('div');
537
+ div.className = 'message';
538
+ const isUser = role === 'user';
539
+ div.innerHTML = `
540
+ <div class="msg-avatar ${role}">${isUser ? 'πŸ‘€' : 'πŸ€–'}</div>
541
+ <div class="msg-content">
542
+ <div class="msg-role">${isUser ? 'Tu' : (state.models[document.getElementById('modelSelect').value]?.name || 'Assistente')}</div>
543
+ <div class="msg-text ${role}">${renderMarkdown(content)}</div>
544
+ <div class="msg-actions">
545
+ <button class="msg-btn" onclick="copyText(this)">πŸ“‹ Copia</button>
546
+ </div>
547
+ </div>`;
548
+ area.appendChild(div);
549
+ area.scrollTop = area.scrollHeight;
550
+ if(save){
551
+ state.sessions[state.currentSession].messages.push({role, content, timestamp: Date.now()});
552
+ saveSessions();
553
+ renderSessions();
554
+ }
555
+ return div.querySelector('.msg-text');
556
+ }
557
+
558
+ function renderMarkdown(text){
559
+ // Minimal markdown: code blocks, inline code, bold, italic, newlines
560
+ text = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
561
+ text = text.replace(/```(\w+)?\n?([\s\S]*?)```/g, (_,lang,code)=>
562
+ `<pre><code class="lang-${lang||''}">${code.trim()}</code></pre>`);
563
+ text = text.replace(/`([^`\n]+)`/g, '<code>$1</code>');
564
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
565
+ text = text.replace(/\*([^*\n]+)\*/g, '<i>$1</i>');
566
+ text = text.replace(/\n/g, '<br>');
567
+ return text;
568
+ }
569
+
570
+ function copyText(btn){
571
+ const text = btn.closest('.msg-content').querySelector('.msg-text').innerText;
572
+ navigator.clipboard.writeText(text);
573
+ showToast('βœ… Copiato negli appunti');
574
+ }
575
+
576
+ function sendSuggestion(text){
577
+ document.getElementById('userInput').value = text;
578
+ sendMessage();
579
+ }
580
+
581
+ async function sendMessage(){
582
+ const input = document.getElementById('userInput');
583
+ const msg = input.value.trim();
584
+ if(!msg || state.isGenerating) return;
585
+
586
+ const model = document.getElementById('modelSelect').value;
587
+ const systemPrompt = document.getElementById('systemPrompt').value;
588
+ const temperature = parseFloat(document.getElementById('temperature').value);
589
+ const maxTokens = parseInt(document.getElementById('maxTokens').value);
590
+ const useMemory = document.getElementById('useMemory').checked;
591
+ const streamMode = document.getElementById('streamMode').checked;
592
+
593
+ input.value = '';
594
+ input.style.height = 'auto';
595
+ updateTokenCount();
596
+ appendMessage('user', msg);
597
+ state.isGenerating = true;
598
+ document.getElementById('sendBtn').disabled = true;
599
+
600
+ // Placeholder assistant
601
+ const assistDiv = appendMessage('assistant', '<span class="typing"></span>', false);
602
+ let fullText = '';
603
+
604
+ try{
605
+ if(streamMode){
606
+ await streamChat({
607
+ message: msg, model, session_id: state.currentSession,
608
+ system_prompt: systemPrompt, temperature, max_tokens: maxTokens,
609
+ use_memory: useMemory
610
+ }, (chunk)=>{
611
+ fullText += chunk;
612
+ assistDiv.innerHTML = renderMarkdown(fullText) + '<span class="typing"></span>';
613
+ document.getElementById('chatArea').scrollTop = document.getElementById('chatArea').scrollHeight;
614
+ });
615
+ assistDiv.innerHTML = renderMarkdown(fullText);
616
+ } else {
617
+ const r = await fetch('/v1/chat', {
618
+ method:'POST',
619
+ headers:{'Content-Type':'application/json'},
620
+ body: JSON.stringify({
621
+ message: msg, model, session_id: state.currentSession,
622
+ system_prompt: systemPrompt, temperature, max_tokens: maxTokens,
623
+ use_memory: useMemory, stream: false
624
+ })
625
+ });
626
+ const data = await r.json();
627
+ fullText = data.response || ('Errore: ' + (data.detail || 'unknown'));
628
+ assistDiv.innerHTML = renderMarkdown(fullText);
629
+ }
630
+ // Salva nella sessione locale
631
+ state.sessions[state.currentSession].messages.push({role:'assistant', content: fullText, timestamp: Date.now()});
632
+ saveSessions();
633
+ renderSessions();
634
+ updateStats();
635
+ }catch(e){
636
+ assistDiv.innerHTML = `<span style="color:var(--error)">❌ Errore: ${e.message}</span>`;
637
+ } finally {
638
+ state.isGenerating = false;
639
+ document.getElementById('sendBtn').disabled = false;
640
+ input.focus();
641
+ }
642
+ }
643
+
644
+ async function streamChat(params, onChunk){
645
+ return new Promise((resolve, reject)=>{
646
+ const qs = new URLSearchParams({
647
+ message: params.message,
648
+ model: params.model,
649
+ session_id: params.session_id,
650
+ system_prompt: params.system_prompt,
651
+ temperature: params.temperature,
652
+ max_tokens: params.max_tokens,
653
+ use_memory: params.use_memory
654
+ });
655
+ const es = new EventSource('/v1/chat/stream?' + qs.toString());
656
+ state.currentEventSource = es;
657
+ es.addEventListener('token', (e)=>{
658
+ try{ const d = JSON.parse(e.data); if(d.token) onChunk(d.token); }catch{}
659
+ });
660
+ es.addEventListener('done', (e)=>{
661
+ es.close();
662
+ state.currentEventSource = null;
663
+ resolve();
664
+ });
665
+ es.addEventListener('error', (e)=>{
666
+ es.close();
667
+ state.currentEventSource = null;
668
+ if(e.data){
669
+ try{ reject(new Error(JSON.parse(e.data).error)); }catch{ reject(new Error('Stream error')); }
670
+ } else {
671
+ // EventSource error senza data β†’ probabilmente fine stream
672
+ resolve();
673
+ }
674
+ });
675
+ });
676
+ }
677
+
678
+ // ═══════════════════════════════════════════════════════
679
+ // STATS
680
+ // ═══════════════════════════════════════════════════════
681
+ async function updateStats(){
682
+ try{
683
+ const r = await fetch('/v1/memory/' + encodeURIComponent(state.currentSession));
684
+ const s = await r.json();
685
+ document.getElementById('statShort').textContent = s.short_memory?.messages || 0;
686
+ document.getElementById('statLong').textContent = s.long_memory?.messages || 0;
687
+ document.getElementById('statTokens').textContent = ((s.total_tokens || 0) / 1000).toFixed(1) + 'k';
688
+ document.getElementById('statTotal').textContent = s.total_messages || 0;
689
+ }catch(e){}
690
+ }
691
+
692
+ // ═══════════════════════════════════════════════════════
693
+ // INPUT
694
+ // ═══════════════════════════════════════════════════════
695
+ function setupInputAutoResize(){
696
+ const input = document.getElementById('userInput');
697
+ input.addEventListener('input', ()=>{
698
+ input.style.height = 'auto';
699
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
700
+ updateTokenCount();
701
+ });
702
+ input.addEventListener('keydown', (e)=>{
703
+ if(e.key === 'Enter' && !e.shiftKey){
704
+ e.preventDefault();
705
+ sendMessage();
706
+ }
707
+ });
708
+ }
709
+
710
+ function updateTokenCount(){
711
+ const v = document.getElementById('userInput').value;
712
+ document.getElementById('tokenCount').textContent = v.length + ' caratteri';
713
+ }
714
+
715
+ // ═══════════════════════════════════════════════════════
716
+ // EXPORT
717
+ // ═══════════════════════════════════════════════════════
718
+ function exportChat(){
719
+ const s = state.sessions[state.currentSession];
720
+ const data = JSON.stringify(s, null, 2);
721
+ const blob = new Blob([data], {type:'application/json'});
722
+ const url = URL.createObjectURL(blob);
723
+ const a = document.createElement('a');
724
+ a.href = url;
725
+ a.download = `chat-${state.currentSession}-${Date.now()}.json`;
726
+ a.click();
727
+ URL.revokeObjectURL(url);
728
+ showToast('βœ… Chat esportata');
729
+ }
730
+
731
+ // ═══════════════════════════════════════════════════════
732
+ // MODAL
733
+ // ═══════════════════════════════════════════════════════
734
+ function openModal(type){
735
+ document.getElementById('modal').classList.add('active');
736
+ const title = document.getElementById('modalTitle');
737
+ const body = document.getElementById('modalBody');
738
+ if(type === 'models'){
739
+ title.textContent = 'πŸ€– Catalogo Modelli NVIDIA NIM';
740
+ let html = '<table class="endpoint-table"><tr><th>Categoria</th><th>Nome</th><th>Model ID</th><th>Context</th></tr>';
741
+ for(const [id,m] of Object.entries(state.models)){
742
+ html += `<tr><td>${m.category}</td><td>${m.name}</td><td><code>${id}</code></td><td>${m.context.toLocaleString()}</td></tr>`;
743
+ }
744
+ html += '</table>';
745
+ body.innerHTML = html;
746
+ } else if(type === 'api'){
747
+ title.textContent = 'πŸ“– API Reference';
748
+ const base = location.origin;
749
+ body.innerHTML = `
750
+ <div class="tab-bar">
751
+ <button class="tab-btn active" onclick="switchTab('curl',this)">cURL</button>
752
+ <button class="tab-btn" onclick="switchTab('python',this)">Python</button>
753
+ <button class="tab-btn" onclick="switchTab('js',this)">JavaScript</button>
754
+ <button class="tab-btn" onclick="switchTab('sse',this)">SSE</button>
755
+ </div>
756
+ <div class="tab-panel active" id="tab-curl">
757
+ <h3>Chat GET</h3>
758
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl "${base}/v1/chat/get?message=Ciao&amp;model=meta/llama-3.1-8b-instruct&amp;session_id=test"</div>
759
+ <h3>Chat POST</h3>
760
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/chat \\
761
+ -H "Content-Type: application/json" \\
762
+ -d '{"message":"Ciao!","session_id":"test"}'</div>
763
+ <h3>SSE Streaming</h3>
764
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -N "${base}/v1/chat/stream?message=Racconta+una+storia"</div>
765
+ <h3>Lista Modelli</h3>
766
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl ${base}/v1/models</div>
767
+ </div>
768
+ <div class="tab-panel" id="tab-python">
769
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>import requests
770
+
771
+ r = requests.post("${base}/v1/chat", json={
772
+ "message": "Ciao!",
773
+ "model": "meta/llama-3.1-405b-instruct",
774
+ "session_id": "python-test",
775
+ "use_memory": True
776
+ })
777
+ print(r.json()["response"])</div>
778
+ <h3>Streaming SSE con httpx</h3>
779
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>import httpx, json
780
+
781
+ with httpx.stream('GET', '${base}/v1/chat/stream',
782
+ params={'message':'Ciao!','session_id':'py'}) as r:
783
+ for line in r.iter_lines():
784
+ if line.startswith('data: '):
785
+ d = json.loads(line[6:])
786
+ if 'token' in d: print(d['token'], end='', flush=True)</div>
787
+ </div>
788
+ <div class="tab-panel" id="tab-js">
789
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>const r = await fetch('${base}/v1/chat', {
790
+ method: 'POST',
791
+ headers: {'Content-Type':'application/json'},
792
+ body: JSON.stringify({
793
+ message: 'Hello!',
794
+ model: 'meta/llama-3.1-405b-instruct',
795
+ session_id: 'js-test'
796
+ })
797
+ });
798
+ const data = await r.json();
799
+ console.log(data.response);</div>
800
+ </div>
801
+ <div class="tab-panel" id="tab-sse">
802
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>const es = new EventSource(
803
+ '${base}/v1/chat/stream?message=Ciao&session_id=sse'
804
+ );
805
+ es.addEventListener('token', (e) =&gt; {
806
+ const {token} = JSON.parse(e.data);
807
+ document.body.innerHTML += token;
808
+ });
809
+ es.addEventListener('done', (e) =&gt; {
810
+ console.log('Fine:', JSON.parse(e.data));
811
+ es.close();
812
+ });</div>
813
+ </div>
814
+ <h3 style="margin-top:20px">πŸ“‹ Tutti gli Endpoints</h3>
815
+ <table class="endpoint-table">
816
+ <tr><th>Metodo</th><th>Endpoint</th><th>Descrizione</th></tr>
817
+ <tr><td><span class="method-badge method-get">GET</span></td><td><code>/v1/models</code></td><td>Lista modelli</td></tr>
818
+ <tr><td><span class="method-badge method-get">GET</span></td><td><code>/v1/chat/get</code></td><td>Chat via GET</td></tr>
819
+ <tr><td><span class="method-badge method-post">POST</span></td><td><code>/v1/chat</code></td><td>Chat via POST</td></tr>
820
+ <tr><td><span class="method-badge method-get">GET</span></td><td><code>/v1/chat/stream</code></td><td>SSE via GET</td></tr>
821
+ <tr><td><span class="method-badge method-post">POST</span></td><td><code>/v1/chat/stream</code></td><td>SSE via POST</td></tr>
822
+ <tr><td><span class="method-badge method-post">POST</span></td><td><code>/v1/memory</code></td><td>Gestione memoria</td></tr>
823
+ <tr><td><span class="method-badge method-post">POST</span></td><td><code>/v1/mcp</code></td><td>MCP JSON-RPC</td></tr>
824
+ </table>
825
+ `;
826
+ } else if(type === 'mcp'){
827
+ title.textContent = 'πŸ”Œ Server MCP (Model Context Protocol)';
828
+ const base = location.origin;
829
+ body.innerHTML = `
830
+ <h3>Configurazione Claude Desktop / Cursor</h3>
831
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>{
832
+ "mcpServers": {
833
+ "nvidia-ai": {
834
+ "url": "${base}/v1/mcp",
835
+ "transport": "http"
836
+ }
837
+ }
838
+ }</div>
839
+
840
+ <h3>Test Initialize</h3>
841
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
842
+ -H "Content-Type: application/json" \\
843
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'</div>
844
+
845
+ <h3>Lista Tools</h3>
846
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
847
+ -H "Content-Type: application/json" \\
848
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'</div>
849
+
850
+ <h3>Chiamata Tool</h3>
851
+ <div class="code-block"><button class="copy-btn" onclick="copyBlock(this)">Copia</button>curl -X POST ${base}/v1/mcp \\
852
+ -H "Content-Type: application/json" \\
853
+ -d '{
854
+ "jsonrpc":"2.0","id":3,
855
+ "method":"tools/call",
856
+ "params":{
857
+ "name":"nvidia_chat",
858
+ "arguments":{
859
+ "message":"Ciao!",
860
+ "model":"meta/llama-3.1-8b-instruct"
861
+ }
862
+ }
863
+ }'</div>
864
+
865
+ <h3>Tools Disponibili</h3>
866
+ <table class="endpoint-table">
867
+ <tr><th>Tool</th><th>Descrizione</th></tr>
868
+ <tr><td><code>nvidia_chat</code></td><td>Chat con qualsiasi modello NVIDIA + memoria</td></tr>
869
+ <tr><td><code>list_models</code></td><td>Lista modelli disponibili</td></tr>
870
+ <tr><td><code>memory_stats</code></td><td>Statistiche memoria sessione</td></tr>
871
+ <tr><td><code>memory_clear</code></td><td>Pulisci memoria</td></tr>
872
+ <tr><td><code>get_conversation_history</code></td><td>Cronologia conversazione</td></tr>
873
+ </table>
874
+ `;
875
+ }
876
+ }
877
+
878
+ function switchTab(id, btn){
879
+ document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
880
+ document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
881
+ btn.classList.add('active');
882
+ document.getElementById('tab-'+id).classList.add('active');
883
+ }
884
+
885
+ function copyBlock(btn){
886
+ const text = btn.parentElement.innerText.replace('Copia','').trim();
887
+ navigator.clipboard.writeText(text);
888
+ btn.textContent = 'βœ…';
889
+ setTimeout(()=>btn.textContent='Copia', 1500);
890
+ }
891
+
892
+ function closeModal(){
893
+ document.getElementById('modal').classList.remove('active');
894
+ }
895
+
896
+ document.getElementById('modal').addEventListener('click', (e)=>{
897
+ if(e.target.id === 'modal') closeModal();
898
+ });
899
+
900
+ // ═══════════════════════════════════════════════════════
901
+ // TOAST
902
+ // ═══════════════════════════════════════════════════════
903
+ function showToast(msg){
904
+ const t = document.getElementById('toast');
905
+ t.textContent = msg;
906
+ t.classList.add('show');
907
+ setTimeout(()=>t.classList.remove('show'), 2500);
908
+ }
909
+
910
+ // GO!
911
+ init();
912
+ </script>
913
+ </body>
914
+ </html>