Really-amin commited on
Commit
73b8257
·
verified ·
1 Parent(s): e19c89a

Upload 301 files

Browse files
ai_tools.html ADDED
@@ -0,0 +1,802 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Tools - Crypto Intelligence Hub</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #050816 0%, #0a1128 100%);
17
+ color: #e2e8f0;
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .header {
23
+ text-align: center;
24
+ margin-bottom: 40px;
25
+ padding: 30px 20px;
26
+ background: rgba(15, 23, 42, 0.6);
27
+ border-radius: 16px;
28
+ backdrop-filter: blur(10px);
29
+ border: 1px solid rgba(255, 255, 255, 0.1);
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 2.5rem;
34
+ font-weight: 700;
35
+ margin-bottom: 10px;
36
+ background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
37
+ -webkit-background-clip: text;
38
+ -webkit-text-fill-color: transparent;
39
+ background-clip: text;
40
+ }
41
+
42
+ .header p {
43
+ color: #94a3b8;
44
+ font-size: 1.1rem;
45
+ }
46
+
47
+ .container {
48
+ max-width: 1400px;
49
+ margin: 0 auto;
50
+ }
51
+
52
+ .card {
53
+ background: rgba(15, 23, 42, 0.8);
54
+ border-radius: 16px;
55
+ padding: 30px;
56
+ margin-bottom: 30px;
57
+ border: 1px solid rgba(255, 255, 255, 0.1);
58
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
59
+ backdrop-filter: blur(10px);
60
+ }
61
+
62
+ .card-title {
63
+ font-size: 1.8rem;
64
+ font-weight: 600;
65
+ margin-bottom: 25px;
66
+ color: #f1f5f9;
67
+ }
68
+
69
+ .form-group {
70
+ margin-bottom: 20px;
71
+ }
72
+
73
+ .form-label {
74
+ display: block;
75
+ margin-bottom: 8px;
76
+ color: #cbd5e1;
77
+ font-weight: 500;
78
+ font-size: 0.95rem;
79
+ }
80
+
81
+ .form-input,
82
+ .form-textarea,
83
+ .form-select {
84
+ width: 100%;
85
+ padding: 12px 16px;
86
+ background: rgba(30, 41, 59, 0.8);
87
+ border: 1px solid rgba(255, 255, 255, 0.15);
88
+ border-radius: 8px;
89
+ color: #e2e8f0;
90
+ font-size: 1rem;
91
+ transition: all 0.3s ease;
92
+ }
93
+
94
+ .form-input:focus,
95
+ .form-textarea:focus,
96
+ .form-select:focus {
97
+ outline: none;
98
+ border-color: #60a5fa;
99
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
100
+ }
101
+
102
+ .form-textarea {
103
+ min-height: 120px;
104
+ resize: vertical;
105
+ font-family: inherit;
106
+ }
107
+
108
+ .btn {
109
+ padding: 12px 24px;
110
+ font-size: 1rem;
111
+ font-weight: 600;
112
+ border: none;
113
+ border-radius: 8px;
114
+ cursor: pointer;
115
+ transition: all 0.3s ease;
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ }
120
+
121
+ .btn-primary {
122
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
123
+ color: white;
124
+ }
125
+
126
+ .btn-primary:hover:not(:disabled) {
127
+ transform: translateY(-2px);
128
+ box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
129
+ }
130
+
131
+ .btn-primary:disabled {
132
+ opacity: 0.6;
133
+ cursor: not-allowed;
134
+ }
135
+
136
+ .btn-secondary {
137
+ background: rgba(71, 85, 105, 0.8);
138
+ color: #e2e8f0;
139
+ border: 1px solid rgba(255, 255, 255, 0.1);
140
+ }
141
+
142
+ .btn-secondary:hover:not(:disabled) {
143
+ background: rgba(100, 116, 139, 0.9);
144
+ }
145
+
146
+ .result-box {
147
+ margin-top: 25px;
148
+ padding: 20px;
149
+ background: rgba(30, 41, 59, 0.6);
150
+ border-radius: 12px;
151
+ border: 1px solid rgba(255, 255, 255, 0.1);
152
+ }
153
+
154
+ .error-box {
155
+ margin-top: 25px;
156
+ padding: 16px;
157
+ background: rgba(239, 68, 68, 0.1);
158
+ border: 1px solid rgba(239, 68, 68, 0.3);
159
+ border-radius: 8px;
160
+ color: #fca5a5;
161
+ }
162
+
163
+ .success-box {
164
+ margin-top: 25px;
165
+ padding: 20px;
166
+ background: rgba(34, 197, 94, 0.1);
167
+ border: 1px solid rgba(34, 197, 94, 0.3);
168
+ border-radius: 12px;
169
+ }
170
+
171
+ .badge {
172
+ display: inline-block;
173
+ padding: 6px 14px;
174
+ border-radius: 20px;
175
+ font-size: 0.9rem;
176
+ font-weight: 600;
177
+ margin-right: 10px;
178
+ }
179
+
180
+ .badge-positive {
181
+ background: rgba(34, 197, 94, 0.2);
182
+ color: #4ade80;
183
+ border: 1px solid rgba(34, 197, 94, 0.3);
184
+ }
185
+
186
+ .badge-negative {
187
+ background: rgba(239, 68, 68, 0.2);
188
+ color: #f87171;
189
+ border: 1px solid rgba(239, 68, 68, 0.3);
190
+ }
191
+
192
+ .badge-neutral {
193
+ background: rgba(148, 163, 184, 0.2);
194
+ color: #94a3b8;
195
+ border: 1px solid rgba(148, 163, 184, 0.3);
196
+ }
197
+
198
+ .badge-success {
199
+ background: rgba(34, 197, 94, 0.2);
200
+ color: #4ade80;
201
+ border: 1px solid rgba(34, 197, 94, 0.3);
202
+ }
203
+
204
+ .badge-danger {
205
+ background: rgba(239, 68, 68, 0.2);
206
+ color: #f87171;
207
+ border: 1px solid rgba(239, 68, 68, 0.3);
208
+ }
209
+
210
+ .score-bar {
211
+ margin-top: 15px;
212
+ }
213
+
214
+ .score-item {
215
+ display: flex;
216
+ align-items: center;
217
+ margin-bottom: 8px;
218
+ }
219
+
220
+ .score-label {
221
+ min-width: 80px;
222
+ font-size: 0.9rem;
223
+ color: #cbd5e1;
224
+ }
225
+
226
+ .score-progress {
227
+ flex: 1;
228
+ height: 8px;
229
+ background: rgba(30, 41, 59, 0.8);
230
+ border-radius: 4px;
231
+ overflow: hidden;
232
+ margin: 0 12px;
233
+ }
234
+
235
+ .score-fill {
236
+ height: 100%;
237
+ background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
238
+ border-radius: 4px;
239
+ transition: width 0.5s ease;
240
+ }
241
+
242
+ .score-value {
243
+ min-width: 50px;
244
+ text-align: right;
245
+ font-weight: 600;
246
+ color: #e2e8f0;
247
+ }
248
+
249
+ .table-container {
250
+ overflow-x: auto;
251
+ margin-top: 20px;
252
+ }
253
+
254
+ table {
255
+ width: 100%;
256
+ border-collapse: collapse;
257
+ }
258
+
259
+ th {
260
+ background: rgba(30, 41, 59, 0.8);
261
+ padding: 12px;
262
+ text-align: left;
263
+ font-weight: 600;
264
+ color: #f1f5f9;
265
+ border-bottom: 2px solid rgba(255, 255, 255, 0.1);
266
+ }
267
+
268
+ td {
269
+ padding: 12px;
270
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
271
+ color: #cbd5e1;
272
+ }
273
+
274
+ tr:hover {
275
+ background: rgba(30, 41, 59, 0.4);
276
+ }
277
+
278
+ .info-box {
279
+ padding: 16px;
280
+ background: rgba(59, 130, 246, 0.1);
281
+ border: 1px solid rgba(59, 130, 246, 0.3);
282
+ border-radius: 8px;
283
+ margin: 15px 0;
284
+ color: #93c5fd;
285
+ }
286
+
287
+ .warning-box {
288
+ padding: 16px;
289
+ background: rgba(251, 191, 36, 0.1);
290
+ border: 1px solid rgba(251, 191, 36, 0.3);
291
+ border-radius: 8px;
292
+ margin: 15px 0;
293
+ color: #fcd34d;
294
+ }
295
+
296
+ .status-grid {
297
+ display: grid;
298
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
299
+ gap: 15px;
300
+ margin: 20px 0;
301
+ }
302
+
303
+ .status-item {
304
+ padding: 15px;
305
+ background: rgba(30, 41, 59, 0.6);
306
+ border-radius: 8px;
307
+ border: 1px solid rgba(255, 255, 255, 0.1);
308
+ }
309
+
310
+ .status-label {
311
+ font-size: 0.85rem;
312
+ color: #94a3b8;
313
+ margin-bottom: 5px;
314
+ }
315
+
316
+ .status-value {
317
+ font-size: 1.3rem;
318
+ font-weight: 700;
319
+ color: #f1f5f9;
320
+ }
321
+
322
+ .summary-text {
323
+ padding: 20px;
324
+ background: rgba(30, 41, 59, 0.8);
325
+ border-radius: 8px;
326
+ border-left: 4px solid #60a5fa;
327
+ font-size: 1.05rem;
328
+ line-height: 1.7;
329
+ color: #e2e8f0;
330
+ margin-bottom: 20px;
331
+ }
332
+
333
+ .sentences-list {
334
+ list-style: none;
335
+ padding: 0;
336
+ }
337
+
338
+ .sentences-list li {
339
+ padding: 12px 15px;
340
+ background: rgba(30, 41, 59, 0.6);
341
+ border-radius: 8px;
342
+ margin-bottom: 10px;
343
+ border-left: 3px solid #8b5cf6;
344
+ color: #cbd5e1;
345
+ }
346
+
347
+ .sentences-list li:before {
348
+ content: "→";
349
+ margin-right: 10px;
350
+ color: #8b5cf6;
351
+ font-weight: bold;
352
+ }
353
+
354
+ .loading {
355
+ display: inline-block;
356
+ width: 16px;
357
+ height: 16px;
358
+ border: 2px solid rgba(255, 255, 255, 0.3);
359
+ border-top-color: #fff;
360
+ border-radius: 50%;
361
+ animation: spin 0.6s linear infinite;
362
+ }
363
+
364
+ @keyframes spin {
365
+ to { transform: rotate(360deg); }
366
+ }
367
+
368
+ .two-column {
369
+ display: grid;
370
+ grid-template-columns: 1fr 1fr;
371
+ gap: 20px;
372
+ }
373
+
374
+ @media (max-width: 768px) {
375
+ .header h1 {
376
+ font-size: 1.8rem;
377
+ }
378
+
379
+ .header p {
380
+ font-size: 0.95rem;
381
+ }
382
+
383
+ .card {
384
+ padding: 20px;
385
+ }
386
+
387
+ .card-title {
388
+ font-size: 1.4rem;
389
+ }
390
+
391
+ .two-column {
392
+ grid-template-columns: 1fr;
393
+ }
394
+
395
+ .status-grid {
396
+ grid-template-columns: 1fr;
397
+ }
398
+ }
399
+
400
+ .hidden {
401
+ display: none;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="container">
407
+ <div class="header">
408
+ <h1>AI Tools – Crypto Intelligence Hub</h1>
409
+ <p>Sentiment, Summaries, and Model Diagnostics</p>
410
+ </div>
411
+
412
+ <!-- Sentiment Playground -->
413
+ <div class="card">
414
+ <h2 class="card-title">Sentiment Playground</h2>
415
+
416
+ <div class="form-group">
417
+ <label class="form-label" for="sentiment-input">Enter Text</label>
418
+ <textarea
419
+ id="sentiment-input"
420
+ class="form-textarea"
421
+ placeholder="Enter text to analyze sentiment (tweets, news, or any text)..."
422
+ ></textarea>
423
+ </div>
424
+
425
+ <div class="two-column">
426
+ <div class="form-group">
427
+ <label class="form-label" for="sentiment-source">Source Type</label>
428
+ <select id="sentiment-source" class="form-select">
429
+ <option value="user">User Input</option>
430
+ <option value="tweet">Tweet</option>
431
+ <option value="news">News</option>
432
+ </select>
433
+ </div>
434
+
435
+ <div class="form-group">
436
+ <label class="form-label" for="sentiment-model-key">Model Key (Optional)</label>
437
+ <input
438
+ type="text"
439
+ id="sentiment-model-key"
440
+ class="form-input"
441
+ placeholder="Leave empty for default model"
442
+ />
443
+ </div>
444
+ </div>
445
+
446
+ <button id="analyze-sentiment-btn" class="btn btn-primary">
447
+ Analyze Sentiment
448
+ </button>
449
+
450
+ <div id="sentiment-result" class="hidden"></div>
451
+ </div>
452
+
453
+ <!-- Text Summarizer -->
454
+ <div class="card">
455
+ <h2 class="card-title">Text Summarizer</h2>
456
+
457
+ <div class="form-group">
458
+ <label class="form-label" for="summary-input">Enter Long Text</label>
459
+ <textarea
460
+ id="summary-input"
461
+ class="form-textarea"
462
+ placeholder="Paste article or long text to summarize..."
463
+ style="min-height: 180px;"
464
+ ></textarea>
465
+ </div>
466
+
467
+ <div class="form-group">
468
+ <label class="form-label" for="max-sentences">Maximum Sentences</label>
469
+ <select id="max-sentences" class="form-select">
470
+ <option value="2">2 sentences</option>
471
+ <option value="3" selected>3 sentences</option>
472
+ <option value="4">4 sentences</option>
473
+ <option value="5">5 sentences</option>
474
+ </select>
475
+ </div>
476
+
477
+ <button id="summarize-btn" class="btn btn-primary">
478
+ Summarize
479
+ </button>
480
+
481
+ <div id="summary-result" class="hidden"></div>
482
+ </div>
483
+
484
+ <!-- Model Status & Diagnostics -->
485
+ <div class="card">
486
+ <h2 class="card-title">Model Status & Diagnostics</h2>
487
+
488
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
489
+ <h3 style="color: #cbd5e1; font-size: 1.2rem;">Registry Status</h3>
490
+ <button id="refresh-status-btn" class="btn btn-secondary">
491
+ Refresh
492
+ </button>
493
+ </div>
494
+
495
+ <div id="registry-status"></div>
496
+
497
+ <h3 style="color: #cbd5e1; font-size: 1.2rem; margin: 30px 0 15px 0;">Models Table</h3>
498
+ <div id="models-table"></div>
499
+ </div>
500
+ </div>
501
+
502
+ <script>
503
+ (function() {
504
+ 'use strict';
505
+
506
+ const AITools = {
507
+ // Sentiment Analysis
508
+ async analyzeSentiment() {
509
+ const text = document.getElementById('sentiment-input').value.trim();
510
+ const source = document.getElementById('sentiment-source').value;
511
+ const modelKey = document.getElementById('sentiment-model-key').value.trim();
512
+ const btn = document.getElementById('analyze-sentiment-btn');
513
+ const resultDiv = document.getElementById('sentiment-result');
514
+
515
+ if (!text) {
516
+ this.showError(resultDiv, 'Please enter text to analyze');
517
+ return;
518
+ }
519
+
520
+ btn.disabled = true;
521
+ btn.innerHTML = '<span class="loading"></span> Analyzing...';
522
+ resultDiv.classList.add('hidden');
523
+
524
+ try {
525
+ const payload = { text, source };
526
+ if (modelKey) payload.model_key = modelKey;
527
+
528
+ const response = await fetch('/api/sentiment/analyze', {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify(payload)
532
+ });
533
+
534
+ const data = await response.json();
535
+
536
+ if (!response.ok || !data.ok) {
537
+ throw new Error(data.error || 'Sentiment analysis failed');
538
+ }
539
+
540
+ this.displaySentimentResult(resultDiv, data);
541
+ } catch (error) {
542
+ this.showError(resultDiv, error.message);
543
+ } finally {
544
+ btn.disabled = false;
545
+ btn.innerHTML = 'Analyze Sentiment';
546
+ }
547
+ },
548
+
549
+ displaySentimentResult(container, data) {
550
+ const label = data.label || 'unknown';
551
+ const score = (data.score * 100).toFixed(1);
552
+ const labelClass = label.toLowerCase();
553
+
554
+ let html = '<div class="result-box">';
555
+ html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>';
556
+ html += `<div style="margin-bottom: 15px;">`;
557
+ html += `<span class="badge badge-${labelClass}">${label.toUpperCase()}</span>`;
558
+ html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0;">${score}%</span>`;
559
+ html += `</div>`;
560
+
561
+ if (data.model) {
562
+ html += `<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 15px;">Model: ${data.model}</p>`;
563
+ }
564
+
565
+ if (data.details && data.details.labels && data.details.scores) {
566
+ html += '<div class="score-bar">';
567
+ for (let i = 0; i < data.details.labels.length; i++) {
568
+ const lbl = data.details.labels[i];
569
+ const scr = (data.details.scores[i] * 100).toFixed(1);
570
+ html += '<div class="score-item">';
571
+ html += `<span class="score-label">${lbl}</span>`;
572
+ html += '<div class="score-progress">';
573
+ html += `<div class="score-fill" style="width: ${scr}%"></div>`;
574
+ html += '</div>';
575
+ html += `<span class="score-value">${scr}%</span>`;
576
+ html += '</div>';
577
+ }
578
+ html += '</div>';
579
+ }
580
+
581
+ html += '</div>';
582
+ container.innerHTML = html;
583
+ container.classList.remove('hidden');
584
+ },
585
+
586
+ // Text Summarization
587
+ async summarizeText() {
588
+ const text = document.getElementById('summary-input').value.trim();
589
+ const maxSentences = parseInt(document.getElementById('max-sentences').value);
590
+ const btn = document.getElementById('summarize-btn');
591
+ const resultDiv = document.getElementById('summary-result');
592
+
593
+ if (!text) {
594
+ this.showError(resultDiv, 'Please enter text to summarize');
595
+ return;
596
+ }
597
+
598
+ btn.disabled = true;
599
+ btn.innerHTML = '<span class="loading"></span> Summarizing...';
600
+ resultDiv.classList.add('hidden');
601
+
602
+ try {
603
+ const response = await fetch('/api/ai/summarize', {
604
+ method: 'POST',
605
+ headers: { 'Content-Type': 'application/json' },
606
+ body: JSON.stringify({ text, max_sentences: maxSentences })
607
+ });
608
+
609
+ const data = await response.json();
610
+
611
+ if (!response.ok || !data.ok) {
612
+ throw new Error(data.error || 'Summarization failed');
613
+ }
614
+
615
+ this.displaySummaryResult(resultDiv, data);
616
+ } catch (error) {
617
+ this.showError(resultDiv, error.message);
618
+ } finally {
619
+ btn.disabled = false;
620
+ btn.innerHTML = 'Summarize';
621
+ }
622
+ },
623
+
624
+ displaySummaryResult(container, data) {
625
+ let html = '<div class="result-box">';
626
+ html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Summary</h3>';
627
+
628
+ if (data.summary) {
629
+ html += `<div class="summary-text">${this.escapeHtml(data.summary)}</div>`;
630
+ }
631
+
632
+ if (data.sentences && data.sentences.length > 0) {
633
+ html += '<h4 style="margin: 20px 0 10px 0; color: #cbd5e1; font-size: 1.1rem;">Key Sentences</h4>';
634
+ html += '<ul class="sentences-list">';
635
+ data.sentences.forEach(sentence => {
636
+ html += `<li>${this.escapeHtml(sentence)}</li>`;
637
+ });
638
+ html += '</ul>';
639
+ }
640
+
641
+ html += '</div>';
642
+ container.innerHTML = html;
643
+ container.classList.remove('hidden');
644
+ },
645
+
646
+ // Model Status & Diagnostics
647
+ async loadModelStatus() {
648
+ const statusDiv = document.getElementById('registry-status');
649
+ const tableDiv = document.getElementById('models-table');
650
+ const btn = document.getElementById('refresh-status-btn');
651
+
652
+ btn.disabled = true;
653
+ btn.innerHTML = '<span class="loading"></span> Loading...';
654
+
655
+ try {
656
+ const [statusRes, listRes] = await Promise.all([
657
+ fetch('/api/models/status'),
658
+ fetch('/api/models/list')
659
+ ]);
660
+
661
+ const statusData = await statusRes.json();
662
+ const listData = await listRes.json();
663
+
664
+ this.displayRegistryStatus(statusDiv, statusData);
665
+ this.displayModelsTable(tableDiv, listData);
666
+ } catch (error) {
667
+ this.showError(statusDiv, 'Failed to load model status: ' + error.message);
668
+ } finally {
669
+ btn.disabled = false;
670
+ btn.innerHTML = 'Refresh';
671
+ }
672
+ },
673
+
674
+ displayRegistryStatus(container, data) {
675
+ let html = '<div class="status-grid">';
676
+
677
+ html += '<div class="status-item">';
678
+ html += '<div class="status-label">HF Mode</div>';
679
+ html += `<div class="status-value">${data.hf_mode || 'unknown'}</div>`;
680
+ html += '</div>';
681
+
682
+ html += '<div class="status-item">';
683
+ html += '<div class="status-label">Overall Status</div>';
684
+ html += `<div class="status-value">${data.status || 'unknown'}</div>`;
685
+ html += '</div>';
686
+
687
+ html += '<div class="status-item">';
688
+ html += '<div class="status-label">Models Loaded</div>';
689
+ html += `<div class="status-value">${data.models_loaded || 0}</div>`;
690
+ html += '</div>';
691
+
692
+ html += '<div class="status-item">';
693
+ html += '<div class="status-label">Models Failed</div>';
694
+ html += `<div class="status-value">${data.models_failed || 0}</div>`;
695
+ html += '</div>';
696
+
697
+ html += '</div>';
698
+
699
+ if (data.status === 'disabled' || data.hf_mode === 'off') {
700
+ html += '<div class="info-box">';
701
+ html += '<strong>Note:</strong> HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.';
702
+ html += '</div>';
703
+ } else if (data.models_loaded === 0 && data.status !== 'disabled') {
704
+ html += '<div class="warning-box">';
705
+ html += '<strong>Warning:</strong> No models could be loaded. Check model IDs or HF credentials.';
706
+ html += '</div>';
707
+ }
708
+
709
+ if (data.error) {
710
+ html += '<div class="error-box" style="margin-top: 15px;">';
711
+ html += `<strong>Error:</strong> ${this.escapeHtml(data.error)}`;
712
+ html += '</div>';
713
+ }
714
+
715
+ if (data.failed && data.failed.length > 0) {
716
+ html += '<div style="margin-top: 20px;">';
717
+ html += '<h4 style="color: #cbd5e1; margin-bottom: 10px;">Failed Models</h4>';
718
+ html += '<div style="background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 15px;">';
719
+ data.failed.forEach(([key, error]) => {
720
+ html += `<div style="margin-bottom: 8px; padding: 8px; background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; border-radius: 4px;">`;
721
+ html += `<strong style="color: #fca5a5;">${key}:</strong> `;
722
+ html += `<span style="color: #cbd5e1;">${this.escapeHtml(error)}</span>`;
723
+ html += `</div>`;
724
+ });
725
+ html += '</div>';
726
+ html += '</div>';
727
+ }
728
+
729
+ container.innerHTML = html;
730
+ },
731
+
732
+ displayModelsTable(container, data) {
733
+ if (!data.models || data.models.length === 0) {
734
+ container.innerHTML = '<div class="info-box">No models configured</div>';
735
+ return;
736
+ }
737
+
738
+ let html = '<div class="table-container">';
739
+ html += '<table>';
740
+ html += '<thead><tr>';
741
+ html += '<th>Key</th>';
742
+ html += '<th>Task</th>';
743
+ html += '<th>Model ID</th>';
744
+ html += '<th>Loaded</th>';
745
+ html += '<th>Error</th>';
746
+ html += '</tr></thead>';
747
+ html += '<tbody>';
748
+
749
+ data.models.forEach(model => {
750
+ html += '<tr>';
751
+ html += `<td><strong>${model.key || 'N/A'}</strong></td>`;
752
+ html += `<td>${model.task || 'N/A'}</td>`;
753
+ html += `<td style="font-family: monospace; font-size: 0.85rem;">${model.model_id || 'N/A'}</td>`;
754
+ html += '<td>';
755
+ if (model.loaded) {
756
+ html += '<span class="badge badge-success">Yes</span>';
757
+ } else {
758
+ html += '<span class="badge badge-danger">No</span>';
759
+ }
760
+ html += '</td>';
761
+ html += `<td style="color: #f87171; font-size: 0.85rem;">${model.error ? this.escapeHtml(model.error) : '-'}</td>`;
762
+ html += '</tr>';
763
+ });
764
+
765
+ html += '</tbody>';
766
+ html += '</table>';
767
+ html += '</div>';
768
+
769
+ container.innerHTML = html;
770
+ },
771
+
772
+ // Utility functions
773
+ showError(container, message) {
774
+ container.innerHTML = `<div class="error-box"><strong>Error:</strong> ${this.escapeHtml(message)}</div>`;
775
+ container.classList.remove('hidden');
776
+ },
777
+
778
+ escapeHtml(text) {
779
+ const div = document.createElement('div');
780
+ div.textContent = text;
781
+ return div.innerHTML;
782
+ },
783
+
784
+ // Initialize
785
+ init() {
786
+ document.getElementById('analyze-sentiment-btn').addEventListener('click', () => this.analyzeSentiment());
787
+ document.getElementById('summarize-btn').addEventListener('click', () => this.summarizeText());
788
+ document.getElementById('refresh-status-btn').addEventListener('click', () => this.loadModelStatus());
789
+
790
+ this.loadModelStatus();
791
+ }
792
+ };
793
+
794
+ if (document.readyState === 'loading') {
795
+ document.addEventListener('DOMContentLoaded', () => AITools.init());
796
+ } else {
797
+ AITools.init();
798
+ }
799
+ })();
800
+ </script>
801
+ </body>
802
+ </html>
api_server_extended.py CHANGED
@@ -379,6 +379,32 @@ async def index():
379
  headers={"Content-Type": "text/html; charset=utf-8"}
380
  )
381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
  # ===== Health & Status Endpoints =====
384
  @app.get("/health")
@@ -1256,6 +1282,8 @@ async def analyze_sentiment(request: Dict[str, Any]):
1256
  raise HTTPException(status_code=400, detail="Text is required")
1257
 
1258
  mode = request.get("mode", "auto").lower()
 
 
1259
  symbol = request.get("symbol")
1260
 
1261
  try:
@@ -1271,19 +1299,39 @@ async def analyze_sentiment(request: Dict[str, Any]):
1271
  # Check if models are available
1272
  if not result.get("available", True):
1273
  return {
1274
- "success": False,
1275
- "available": False,
1276
- "sentiment": "neutral",
1277
- "confidence": 0.0,
1278
- "mode": mode,
1279
  "error": result.get("error", "Models not available"),
1280
- "reason": "model_unavailable"
 
1281
  }
1282
 
1283
  sentiment_label = result.get("label", "neutral")
1284
  confidence = result.get("confidence", result.get("score", 0.5))
1285
  model_used = result.get("model_count", result.get("model", "unknown"))
1286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1287
  # Save to database
1288
  try:
1289
  conn = sqlite3.connect(str(DB_PATH))
@@ -1303,30 +1351,17 @@ async def analyze_sentiment(request: Dict[str, Any]):
1303
  ))
1304
  conn.commit()
1305
  conn.close()
1306
- saved_to_db = True
1307
  except Exception as db_error:
1308
  logger.warning(f"Failed to save to database: {db_error}")
1309
- saved_to_db = False
1310
 
1311
- return {
1312
- "success": True,
1313
- "available": True,
1314
- "sentiment": sentiment_label,
1315
- "confidence": confidence,
1316
- "mode": mode,
1317
- "result": result,
1318
- "saved_to_db": saved_to_db
1319
- }
1320
 
1321
  except ModelNotAvailable as e:
1322
  return {
1323
- "success": False,
1324
- "available": False,
1325
- "sentiment": "neutral",
1326
- "confidence": 0.0,
1327
- "mode": mode,
1328
  "error": str(e),
1329
- "reason": "model_unavailable"
 
1330
  }
1331
 
1332
  except HTTPException:
@@ -1335,6 +1370,120 @@ async def analyze_sentiment(request: Dict[str, Any]):
1335
  raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
1336
 
1337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1338
  @app.post("/api/news/analyze")
1339
  async def analyze_news(request: Dict[str, Any]):
1340
  """Analyze news article sentiment using HF models"""
@@ -1561,6 +1710,11 @@ async def get_models_status():
1561
  status = "unknown"
1562
  status_message = "Unknown status"
1563
 
 
 
 
 
 
1564
  return {
1565
  "success": True,
1566
  "status": status,
@@ -1572,7 +1726,8 @@ async def get_models_status():
1572
  "initialized": _registry._initialized,
1573
  "models": model_info,
1574
  "registry": registry_info,
1575
- "failed_models": list(_registry._failed_models.keys())[:10], # Limit for brevity
 
1576
  "loaded_models": list(_registry._pipelines.keys()),
1577
  "database": {
1578
  "path": str(DB_PATH),
@@ -1585,7 +1740,10 @@ async def get_models_status():
1585
  "success": False,
1586
  "status": "error",
1587
  "status_message": f"Error retrieving model status: {str(e)}",
1588
- "error": str(e)
 
 
 
1589
  }
1590
 
1591
 
@@ -1612,18 +1770,26 @@ async def initialize_ai_models():
1612
  async def list_available_models():
1613
  """List all available Hugging Face models as data sources"""
1614
  try:
1615
- from ai_models import get_model_info, MODEL_SPECS, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
1616
 
1617
  model_info = get_model_info()
1618
 
1619
  models_list = []
1620
  for key, spec in MODEL_SPECS.items():
 
 
 
 
 
1621
  models_list.append({
1622
- "id": key,
 
1623
  "model_id": spec.model_id,
1624
  "task": spec.task,
1625
  "category": spec.category,
1626
  "requires_auth": spec.requires_auth,
 
 
1627
  "endpoint": f"/api/models/{key}/predict"
1628
  })
1629
 
 
379
  headers={"Content-Type": "text/html; charset=utf-8"}
380
  )
381
 
382
+ @app.get("/ai-tools", response_class=HTMLResponse)
383
+ async def ai_tools_page(request: Request):
384
+ """
385
+ Serve the standalone AI Tools page.
386
+
387
+ This page provides:
388
+ - Sentiment Playground: POST /api/sentiment/analyze
389
+ - Text Summarizer: POST /api/ai/summarize
390
+ - Model Status & Diagnostics: GET /api/models/status, /api/models/list
391
+ """
392
+ ai_tools_path = WORKSPACE_ROOT / "templates" / "ai_tools.html"
393
+ if ai_tools_path.exists():
394
+ content = ai_tools_path.read_text(encoding="utf-8", errors="ignore")
395
+ return HTMLResponse(
396
+ content=content,
397
+ media_type="text/html",
398
+ headers={
399
+ "Content-Type": "text/html; charset=utf-8",
400
+ "X-Content-Type-Options": "nosniff"
401
+ }
402
+ )
403
+ return HTMLResponse(
404
+ "<h1>AI Tools page not found</h1>",
405
+ headers={"Content-Type": "text/html; charset=utf-8"}
406
+ )
407
+
408
 
409
  # ===== Health & Status Endpoints =====
410
  @app.get("/health")
 
1282
  raise HTTPException(status_code=400, detail="Text is required")
1283
 
1284
  mode = request.get("mode", "auto").lower()
1285
+ source = request.get("source", "user")
1286
+ model_key = request.get("model_key")
1287
  symbol = request.get("symbol")
1288
 
1289
  try:
 
1299
  # Check if models are available
1300
  if not result.get("available", True):
1301
  return {
1302
+ "ok": False,
 
 
 
 
1303
  "error": result.get("error", "Models not available"),
1304
+ "label": "neutral",
1305
+ "score": 0.0
1306
  }
1307
 
1308
  sentiment_label = result.get("label", "neutral")
1309
  confidence = result.get("confidence", result.get("score", 0.5))
1310
  model_used = result.get("model_count", result.get("model", "unknown"))
1311
 
1312
+ # Prepare response compatible with ai_tools.html format
1313
+ response_data = {
1314
+ "ok": True,
1315
+ "label": sentiment_label.lower(),
1316
+ "score": float(confidence),
1317
+ "model": f"{model_used} models" if isinstance(model_used, int) else str(model_used)
1318
+ }
1319
+
1320
+ # Add details if available for score bars
1321
+ if result.get("scores"):
1322
+ scores_dict = result.get("scores", {})
1323
+ if isinstance(scores_dict, dict):
1324
+ labels_list = []
1325
+ scores_list = []
1326
+ for lbl, scr in scores_dict.items():
1327
+ labels_list.append(lbl)
1328
+ scores_list.append(float(scr))
1329
+ if labels_list:
1330
+ response_data["details"] = {
1331
+ "labels": labels_list,
1332
+ "scores": scores_list
1333
+ }
1334
+
1335
  # Save to database
1336
  try:
1337
  conn = sqlite3.connect(str(DB_PATH))
 
1351
  ))
1352
  conn.commit()
1353
  conn.close()
 
1354
  except Exception as db_error:
1355
  logger.warning(f"Failed to save to database: {db_error}")
 
1356
 
1357
+ return response_data
 
 
 
 
 
 
 
 
1358
 
1359
  except ModelNotAvailable as e:
1360
  return {
1361
+ "ok": False,
 
 
 
 
1362
  "error": str(e),
1363
+ "label": "neutral",
1364
+ "score": 0.0
1365
  }
1366
 
1367
  except HTTPException:
 
1370
  raise HTTPException(status_code=500, detail=f"Sentiment analysis failed: {str(e)}")
1371
 
1372
 
1373
+ @app.post("/api/ai/summarize")
1374
+ async def summarize_text(request: Dict[str, Any]):
1375
+ """
1376
+ Summarize text using Hugging Face models or simple text processing.
1377
+
1378
+ Expects: { "text": "string", "max_sentences": 3 }
1379
+ Returns: { "ok": true, "summary": "...", "sentences": ["...", "..."] }
1380
+ """
1381
+ try:
1382
+ text = request.get("text", "").strip()
1383
+ max_sentences = request.get("max_sentences", 3)
1384
+
1385
+ if not text:
1386
+ return {
1387
+ "ok": False,
1388
+ "error": "Text is required"
1389
+ }
1390
+
1391
+ # Try to use Hugging Face summarization model if available
1392
+ try:
1393
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
1394
+
1395
+ # Check if summarization model is available
1396
+ summarization_key = None
1397
+ for key, spec in MODEL_SPECS.items():
1398
+ if spec.task == "summarization":
1399
+ summarization_key = key
1400
+ break
1401
+
1402
+ if summarization_key:
1403
+ try:
1404
+ pipeline = _registry.get_pipeline(summarization_key)
1405
+ # Use HF model for summarization
1406
+ # Try with parameters first, then fallback to simple call
1407
+ try:
1408
+ summary_result = pipeline(text, max_length=max_sentences * 50, min_length=max_sentences * 20, do_sample=False)
1409
+ except TypeError:
1410
+ # Some pipelines don't accept these parameters
1411
+ summary_result = pipeline(text)
1412
+
1413
+ if isinstance(summary_result, list) and summary_result:
1414
+ summary_text = summary_result[0].get("summary_text", summary_result[0].get("generated_text", str(summary_result[0])))
1415
+ elif isinstance(summary_result, dict):
1416
+ summary_text = summary_result.get("summary_text", summary_result.get("generated_text", str(summary_result)))
1417
+ else:
1418
+ summary_text = str(summary_result)
1419
+
1420
+ # Split into sentences
1421
+ sentences = [s.strip() + ("." if not s.strip().endswith((".", "!", "?")) else "") for s in summary_text.split(". ") if s.strip()]
1422
+ sentences = sentences[:max_sentences]
1423
+
1424
+ return {
1425
+ "ok": True,
1426
+ "summary": summary_text,
1427
+ "sentences": sentences
1428
+ }
1429
+ except ModelNotAvailable:
1430
+ # Fall through to simple summarizer
1431
+ pass
1432
+ except Exception as e:
1433
+ logger.warning(f"HF summarization failed: {e}, using fallback")
1434
+ # Fall through to simple summarizer
1435
+ pass
1436
+ except Exception as e:
1437
+ logger.warning(f"HF summarization model not available: {e}")
1438
+ # Fall through to simple summarizer
1439
+
1440
+ # Simple placeholder summarizer: split by sentences and take first N
1441
+ sentences = []
1442
+ current_sentence = ""
1443
+
1444
+ for char in text:
1445
+ current_sentence += char
1446
+ if char in ".!?":
1447
+ sentence = current_sentence.strip()
1448
+ if sentence:
1449
+ sentences.append(sentence)
1450
+ current_sentence = ""
1451
+ if len(sentences) >= max_sentences:
1452
+ break
1453
+
1454
+ # If we didn't get enough sentences, add the rest
1455
+ if len(sentences) < max_sentences and current_sentence.strip():
1456
+ sentences.append(current_sentence.strip())
1457
+
1458
+ # If still no sentences, just truncate
1459
+ if not sentences:
1460
+ words = text.split()
1461
+ chunk_size = len(words) // max_sentences
1462
+ sentences = []
1463
+ for i in range(max_sentences):
1464
+ start_idx = i * chunk_size
1465
+ end_idx = start_idx + chunk_size if i < max_sentences - 1 else len(words)
1466
+ if start_idx < len(words):
1467
+ sentence = " ".join(words[start_idx:end_idx])
1468
+ if sentence:
1469
+ sentences.append(sentence)
1470
+
1471
+ summary = " ".join(sentences)
1472
+
1473
+ return {
1474
+ "ok": True,
1475
+ "summary": summary,
1476
+ "sentences": sentences[:max_sentences]
1477
+ }
1478
+
1479
+ except Exception as e:
1480
+ logger.error(f"Summarization failed: {e}")
1481
+ return {
1482
+ "ok": False,
1483
+ "error": f"Summarization failed: {str(e)}"
1484
+ }
1485
+
1486
+
1487
  @app.post("/api/news/analyze")
1488
  async def analyze_news(request: Dict[str, Any]):
1489
  """Analyze news article sentiment using HF models"""
 
1710
  status = "unknown"
1711
  status_message = "Unknown status"
1712
 
1713
+ # Format failed models as list of [key, error] tuples for ai_tools.html
1714
+ failed_list = []
1715
+ for key, error in list(_registry._failed_models.items())[:10]:
1716
+ failed_list.append([key, str(error)])
1717
+
1718
  return {
1719
  "success": True,
1720
  "status": status,
 
1726
  "initialized": _registry._initialized,
1727
  "models": model_info,
1728
  "registry": registry_info,
1729
+ "failed": failed_list, # Format: [[key, error], ...] for ai_tools.html
1730
+ "failed_models": list(_registry._failed_models.keys())[:10], # Keep for backward compatibility
1731
  "loaded_models": list(_registry._pipelines.keys()),
1732
  "database": {
1733
  "path": str(DB_PATH),
 
1740
  "success": False,
1741
  "status": "error",
1742
  "status_message": f"Error retrieving model status: {str(e)}",
1743
+ "error": str(e),
1744
+ "hf_mode": "unknown",
1745
+ "models_loaded": 0,
1746
+ "models_failed": 0
1747
  }
1748
 
1749
 
 
1770
  async def list_available_models():
1771
  """List all available Hugging Face models as data sources"""
1772
  try:
1773
+ from ai_models import get_model_info, MODEL_SPECS, _registry, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
1774
 
1775
  model_info = get_model_info()
1776
 
1777
  models_list = []
1778
  for key, spec in MODEL_SPECS.items():
1779
+ is_loaded = key in _registry._pipelines
1780
+ error_msg = None
1781
+ if key in _registry._failed_models:
1782
+ error_msg = str(_registry._failed_models[key])
1783
+
1784
  models_list.append({
1785
+ "key": key, # ai_tools.html expects "key"
1786
+ "id": key, # Keep for backward compatibility
1787
  "model_id": spec.model_id,
1788
  "task": spec.task,
1789
  "category": spec.category,
1790
  "requires_auth": spec.requires_auth,
1791
+ "loaded": is_loaded, # ai_tools.html expects "loaded" boolean
1792
+ "error": error_msg, # ai_tools.html expects "error" string or null
1793
  "endpoint": f"/api/models/{key}/predict"
1794
  })
1795
 
templates/ai_tools.html ADDED
@@ -0,0 +1,802 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Tools - Crypto Intelligence Hub</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: linear-gradient(135deg, #050816 0%, #0a1128 100%);
17
+ color: #e2e8f0;
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .header {
23
+ text-align: center;
24
+ margin-bottom: 40px;
25
+ padding: 30px 20px;
26
+ background: rgba(15, 23, 42, 0.6);
27
+ border-radius: 16px;
28
+ backdrop-filter: blur(10px);
29
+ border: 1px solid rgba(255, 255, 255, 0.1);
30
+ }
31
+
32
+ .header h1 {
33
+ font-size: 2.5rem;
34
+ font-weight: 700;
35
+ margin-bottom: 10px;
36
+ background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
37
+ -webkit-background-clip: text;
38
+ -webkit-text-fill-color: transparent;
39
+ background-clip: text;
40
+ }
41
+
42
+ .header p {
43
+ color: #94a3b8;
44
+ font-size: 1.1rem;
45
+ }
46
+
47
+ .container {
48
+ max-width: 1400px;
49
+ margin: 0 auto;
50
+ }
51
+
52
+ .card {
53
+ background: rgba(15, 23, 42, 0.8);
54
+ border-radius: 16px;
55
+ padding: 30px;
56
+ margin-bottom: 30px;
57
+ border: 1px solid rgba(255, 255, 255, 0.1);
58
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
59
+ backdrop-filter: blur(10px);
60
+ }
61
+
62
+ .card-title {
63
+ font-size: 1.8rem;
64
+ font-weight: 600;
65
+ margin-bottom: 25px;
66
+ color: #f1f5f9;
67
+ }
68
+
69
+ .form-group {
70
+ margin-bottom: 20px;
71
+ }
72
+
73
+ .form-label {
74
+ display: block;
75
+ margin-bottom: 8px;
76
+ color: #cbd5e1;
77
+ font-weight: 500;
78
+ font-size: 0.95rem;
79
+ }
80
+
81
+ .form-input,
82
+ .form-textarea,
83
+ .form-select {
84
+ width: 100%;
85
+ padding: 12px 16px;
86
+ background: rgba(30, 41, 59, 0.8);
87
+ border: 1px solid rgba(255, 255, 255, 0.15);
88
+ border-radius: 8px;
89
+ color: #e2e8f0;
90
+ font-size: 1rem;
91
+ transition: all 0.3s ease;
92
+ }
93
+
94
+ .form-input:focus,
95
+ .form-textarea:focus,
96
+ .form-select:focus {
97
+ outline: none;
98
+ border-color: #60a5fa;
99
+ box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
100
+ }
101
+
102
+ .form-textarea {
103
+ min-height: 120px;
104
+ resize: vertical;
105
+ font-family: inherit;
106
+ }
107
+
108
+ .btn {
109
+ padding: 12px 24px;
110
+ font-size: 1rem;
111
+ font-weight: 600;
112
+ border: none;
113
+ border-radius: 8px;
114
+ cursor: pointer;
115
+ transition: all 0.3s ease;
116
+ display: inline-flex;
117
+ align-items: center;
118
+ gap: 8px;
119
+ }
120
+
121
+ .btn-primary {
122
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
123
+ color: white;
124
+ }
125
+
126
+ .btn-primary:hover:not(:disabled) {
127
+ transform: translateY(-2px);
128
+ box-shadow: 0 8px 20px rgba(59, 130, 246, 0.4);
129
+ }
130
+
131
+ .btn-primary:disabled {
132
+ opacity: 0.6;
133
+ cursor: not-allowed;
134
+ }
135
+
136
+ .btn-secondary {
137
+ background: rgba(71, 85, 105, 0.8);
138
+ color: #e2e8f0;
139
+ border: 1px solid rgba(255, 255, 255, 0.1);
140
+ }
141
+
142
+ .btn-secondary:hover:not(:disabled) {
143
+ background: rgba(100, 116, 139, 0.9);
144
+ }
145
+
146
+ .result-box {
147
+ margin-top: 25px;
148
+ padding: 20px;
149
+ background: rgba(30, 41, 59, 0.6);
150
+ border-radius: 12px;
151
+ border: 1px solid rgba(255, 255, 255, 0.1);
152
+ }
153
+
154
+ .error-box {
155
+ margin-top: 25px;
156
+ padding: 16px;
157
+ background: rgba(239, 68, 68, 0.1);
158
+ border: 1px solid rgba(239, 68, 68, 0.3);
159
+ border-radius: 8px;
160
+ color: #fca5a5;
161
+ }
162
+
163
+ .success-box {
164
+ margin-top: 25px;
165
+ padding: 20px;
166
+ background: rgba(34, 197, 94, 0.1);
167
+ border: 1px solid rgba(34, 197, 94, 0.3);
168
+ border-radius: 12px;
169
+ }
170
+
171
+ .badge {
172
+ display: inline-block;
173
+ padding: 6px 14px;
174
+ border-radius: 20px;
175
+ font-size: 0.9rem;
176
+ font-weight: 600;
177
+ margin-right: 10px;
178
+ }
179
+
180
+ .badge-positive {
181
+ background: rgba(34, 197, 94, 0.2);
182
+ color: #4ade80;
183
+ border: 1px solid rgba(34, 197, 94, 0.3);
184
+ }
185
+
186
+ .badge-negative {
187
+ background: rgba(239, 68, 68, 0.2);
188
+ color: #f87171;
189
+ border: 1px solid rgba(239, 68, 68, 0.3);
190
+ }
191
+
192
+ .badge-neutral {
193
+ background: rgba(148, 163, 184, 0.2);
194
+ color: #94a3b8;
195
+ border: 1px solid rgba(148, 163, 184, 0.3);
196
+ }
197
+
198
+ .badge-success {
199
+ background: rgba(34, 197, 94, 0.2);
200
+ color: #4ade80;
201
+ border: 1px solid rgba(34, 197, 94, 0.3);
202
+ }
203
+
204
+ .badge-danger {
205
+ background: rgba(239, 68, 68, 0.2);
206
+ color: #f87171;
207
+ border: 1px solid rgba(239, 68, 68, 0.3);
208
+ }
209
+
210
+ .score-bar {
211
+ margin-top: 15px;
212
+ }
213
+
214
+ .score-item {
215
+ display: flex;
216
+ align-items: center;
217
+ margin-bottom: 8px;
218
+ }
219
+
220
+ .score-label {
221
+ min-width: 80px;
222
+ font-size: 0.9rem;
223
+ color: #cbd5e1;
224
+ }
225
+
226
+ .score-progress {
227
+ flex: 1;
228
+ height: 8px;
229
+ background: rgba(30, 41, 59, 0.8);
230
+ border-radius: 4px;
231
+ overflow: hidden;
232
+ margin: 0 12px;
233
+ }
234
+
235
+ .score-fill {
236
+ height: 100%;
237
+ background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
238
+ border-radius: 4px;
239
+ transition: width 0.5s ease;
240
+ }
241
+
242
+ .score-value {
243
+ min-width: 50px;
244
+ text-align: right;
245
+ font-weight: 600;
246
+ color: #e2e8f0;
247
+ }
248
+
249
+ .table-container {
250
+ overflow-x: auto;
251
+ margin-top: 20px;
252
+ }
253
+
254
+ table {
255
+ width: 100%;
256
+ border-collapse: collapse;
257
+ }
258
+
259
+ th {
260
+ background: rgba(30, 41, 59, 0.8);
261
+ padding: 12px;
262
+ text-align: left;
263
+ font-weight: 600;
264
+ color: #f1f5f9;
265
+ border-bottom: 2px solid rgba(255, 255, 255, 0.1);
266
+ }
267
+
268
+ td {
269
+ padding: 12px;
270
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
271
+ color: #cbd5e1;
272
+ }
273
+
274
+ tr:hover {
275
+ background: rgba(30, 41, 59, 0.4);
276
+ }
277
+
278
+ .info-box {
279
+ padding: 16px;
280
+ background: rgba(59, 130, 246, 0.1);
281
+ border: 1px solid rgba(59, 130, 246, 0.3);
282
+ border-radius: 8px;
283
+ margin: 15px 0;
284
+ color: #93c5fd;
285
+ }
286
+
287
+ .warning-box {
288
+ padding: 16px;
289
+ background: rgba(251, 191, 36, 0.1);
290
+ border: 1px solid rgba(251, 191, 36, 0.3);
291
+ border-radius: 8px;
292
+ margin: 15px 0;
293
+ color: #fcd34d;
294
+ }
295
+
296
+ .status-grid {
297
+ display: grid;
298
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
299
+ gap: 15px;
300
+ margin: 20px 0;
301
+ }
302
+
303
+ .status-item {
304
+ padding: 15px;
305
+ background: rgba(30, 41, 59, 0.6);
306
+ border-radius: 8px;
307
+ border: 1px solid rgba(255, 255, 255, 0.1);
308
+ }
309
+
310
+ .status-label {
311
+ font-size: 0.85rem;
312
+ color: #94a3b8;
313
+ margin-bottom: 5px;
314
+ }
315
+
316
+ .status-value {
317
+ font-size: 1.3rem;
318
+ font-weight: 700;
319
+ color: #f1f5f9;
320
+ }
321
+
322
+ .summary-text {
323
+ padding: 20px;
324
+ background: rgba(30, 41, 59, 0.8);
325
+ border-radius: 8px;
326
+ border-left: 4px solid #60a5fa;
327
+ font-size: 1.05rem;
328
+ line-height: 1.7;
329
+ color: #e2e8f0;
330
+ margin-bottom: 20px;
331
+ }
332
+
333
+ .sentences-list {
334
+ list-style: none;
335
+ padding: 0;
336
+ }
337
+
338
+ .sentences-list li {
339
+ padding: 12px 15px;
340
+ background: rgba(30, 41, 59, 0.6);
341
+ border-radius: 8px;
342
+ margin-bottom: 10px;
343
+ border-left: 3px solid #8b5cf6;
344
+ color: #cbd5e1;
345
+ }
346
+
347
+ .sentences-list li:before {
348
+ content: "→";
349
+ margin-right: 10px;
350
+ color: #8b5cf6;
351
+ font-weight: bold;
352
+ }
353
+
354
+ .loading {
355
+ display: inline-block;
356
+ width: 16px;
357
+ height: 16px;
358
+ border: 2px solid rgba(255, 255, 255, 0.3);
359
+ border-top-color: #fff;
360
+ border-radius: 50%;
361
+ animation: spin 0.6s linear infinite;
362
+ }
363
+
364
+ @keyframes spin {
365
+ to { transform: rotate(360deg); }
366
+ }
367
+
368
+ .two-column {
369
+ display: grid;
370
+ grid-template-columns: 1fr 1fr;
371
+ gap: 20px;
372
+ }
373
+
374
+ @media (max-width: 768px) {
375
+ .header h1 {
376
+ font-size: 1.8rem;
377
+ }
378
+
379
+ .header p {
380
+ font-size: 0.95rem;
381
+ }
382
+
383
+ .card {
384
+ padding: 20px;
385
+ }
386
+
387
+ .card-title {
388
+ font-size: 1.4rem;
389
+ }
390
+
391
+ .two-column {
392
+ grid-template-columns: 1fr;
393
+ }
394
+
395
+ .status-grid {
396
+ grid-template-columns: 1fr;
397
+ }
398
+ }
399
+
400
+ .hidden {
401
+ display: none;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="container">
407
+ <div class="header">
408
+ <h1>AI Tools – Crypto Intelligence Hub</h1>
409
+ <p>Sentiment, Summaries, and Model Diagnostics</p>
410
+ </div>
411
+
412
+ <!-- Sentiment Playground -->
413
+ <div class="card">
414
+ <h2 class="card-title">Sentiment Playground</h2>
415
+
416
+ <div class="form-group">
417
+ <label class="form-label" for="sentiment-input">Enter Text</label>
418
+ <textarea
419
+ id="sentiment-input"
420
+ class="form-textarea"
421
+ placeholder="Enter text to analyze sentiment (tweets, news, or any text)..."
422
+ ></textarea>
423
+ </div>
424
+
425
+ <div class="two-column">
426
+ <div class="form-group">
427
+ <label class="form-label" for="sentiment-source">Source Type</label>
428
+ <select id="sentiment-source" class="form-select">
429
+ <option value="user">User Input</option>
430
+ <option value="tweet">Tweet</option>
431
+ <option value="news">News</option>
432
+ </select>
433
+ </div>
434
+
435
+ <div class="form-group">
436
+ <label class="form-label" for="sentiment-model-key">Model Key (Optional)</label>
437
+ <input
438
+ type="text"
439
+ id="sentiment-model-key"
440
+ class="form-input"
441
+ placeholder="Leave empty for default model"
442
+ />
443
+ </div>
444
+ </div>
445
+
446
+ <button id="analyze-sentiment-btn" class="btn btn-primary">
447
+ Analyze Sentiment
448
+ </button>
449
+
450
+ <div id="sentiment-result" class="hidden"></div>
451
+ </div>
452
+
453
+ <!-- Text Summarizer -->
454
+ <div class="card">
455
+ <h2 class="card-title">Text Summarizer</h2>
456
+
457
+ <div class="form-group">
458
+ <label class="form-label" for="summary-input">Enter Long Text</label>
459
+ <textarea
460
+ id="summary-input"
461
+ class="form-textarea"
462
+ placeholder="Paste article or long text to summarize..."
463
+ style="min-height: 180px;"
464
+ ></textarea>
465
+ </div>
466
+
467
+ <div class="form-group">
468
+ <label class="form-label" for="max-sentences">Maximum Sentences</label>
469
+ <select id="max-sentences" class="form-select">
470
+ <option value="2">2 sentences</option>
471
+ <option value="3" selected>3 sentences</option>
472
+ <option value="4">4 sentences</option>
473
+ <option value="5">5 sentences</option>
474
+ </select>
475
+ </div>
476
+
477
+ <button id="summarize-btn" class="btn btn-primary">
478
+ Summarize
479
+ </button>
480
+
481
+ <div id="summary-result" class="hidden"></div>
482
+ </div>
483
+
484
+ <!-- Model Status & Diagnostics -->
485
+ <div class="card">
486
+ <h2 class="card-title">Model Status & Diagnostics</h2>
487
+
488
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
489
+ <h3 style="color: #cbd5e1; font-size: 1.2rem;">Registry Status</h3>
490
+ <button id="refresh-status-btn" class="btn btn-secondary">
491
+ Refresh
492
+ </button>
493
+ </div>
494
+
495
+ <div id="registry-status"></div>
496
+
497
+ <h3 style="color: #cbd5e1; font-size: 1.2rem; margin: 30px 0 15px 0;">Models Table</h3>
498
+ <div id="models-table"></div>
499
+ </div>
500
+ </div>
501
+
502
+ <script>
503
+ (function() {
504
+ 'use strict';
505
+
506
+ const AITools = {
507
+ // Sentiment Analysis
508
+ async analyzeSentiment() {
509
+ const text = document.getElementById('sentiment-input').value.trim();
510
+ const source = document.getElementById('sentiment-source').value;
511
+ const modelKey = document.getElementById('sentiment-model-key').value.trim();
512
+ const btn = document.getElementById('analyze-sentiment-btn');
513
+ const resultDiv = document.getElementById('sentiment-result');
514
+
515
+ if (!text) {
516
+ this.showError(resultDiv, 'Please enter text to analyze');
517
+ return;
518
+ }
519
+
520
+ btn.disabled = true;
521
+ btn.innerHTML = '<span class="loading"></span> Analyzing...';
522
+ resultDiv.classList.add('hidden');
523
+
524
+ try {
525
+ const payload = { text, source };
526
+ if (modelKey) payload.model_key = modelKey;
527
+
528
+ const response = await fetch('/api/sentiment/analyze', {
529
+ method: 'POST',
530
+ headers: { 'Content-Type': 'application/json' },
531
+ body: JSON.stringify(payload)
532
+ });
533
+
534
+ const data = await response.json();
535
+
536
+ if (!response.ok || !data.ok) {
537
+ throw new Error(data.error || 'Sentiment analysis failed');
538
+ }
539
+
540
+ this.displaySentimentResult(resultDiv, data);
541
+ } catch (error) {
542
+ this.showError(resultDiv, error.message);
543
+ } finally {
544
+ btn.disabled = false;
545
+ btn.innerHTML = 'Analyze Sentiment';
546
+ }
547
+ },
548
+
549
+ displaySentimentResult(container, data) {
550
+ const label = data.label || 'unknown';
551
+ const score = (data.score * 100).toFixed(1);
552
+ const labelClass = label.toLowerCase();
553
+
554
+ let html = '<div class="result-box">';
555
+ html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Sentiment Analysis Result</h3>';
556
+ html += `<div style="margin-bottom: 15px;">`;
557
+ html += `<span class="badge badge-${labelClass}">${label.toUpperCase()}</span>`;
558
+ html += `<span style="font-size: 1.3rem; font-weight: 700; color: #e2e8f0;">${score}%</span>`;
559
+ html += `</div>`;
560
+
561
+ if (data.model) {
562
+ html += `<p style="color: #94a3b8; font-size: 0.9rem; margin-bottom: 15px;">Model: ${data.model}</p>`;
563
+ }
564
+
565
+ if (data.details && data.details.labels && data.details.scores) {
566
+ html += '<div class="score-bar">';
567
+ for (let i = 0; i < data.details.labels.length; i++) {
568
+ const lbl = data.details.labels[i];
569
+ const scr = (data.details.scores[i] * 100).toFixed(1);
570
+ html += '<div class="score-item">';
571
+ html += `<span class="score-label">${lbl}</span>`;
572
+ html += '<div class="score-progress">';
573
+ html += `<div class="score-fill" style="width: ${scr}%"></div>`;
574
+ html += '</div>';
575
+ html += `<span class="score-value">${scr}%</span>`;
576
+ html += '</div>';
577
+ }
578
+ html += '</div>';
579
+ }
580
+
581
+ html += '</div>';
582
+ container.innerHTML = html;
583
+ container.classList.remove('hidden');
584
+ },
585
+
586
+ // Text Summarization
587
+ async summarizeText() {
588
+ const text = document.getElementById('summary-input').value.trim();
589
+ const maxSentences = parseInt(document.getElementById('max-sentences').value);
590
+ const btn = document.getElementById('summarize-btn');
591
+ const resultDiv = document.getElementById('summary-result');
592
+
593
+ if (!text) {
594
+ this.showError(resultDiv, 'Please enter text to summarize');
595
+ return;
596
+ }
597
+
598
+ btn.disabled = true;
599
+ btn.innerHTML = '<span class="loading"></span> Summarizing...';
600
+ resultDiv.classList.add('hidden');
601
+
602
+ try {
603
+ const response = await fetch('/api/ai/summarize', {
604
+ method: 'POST',
605
+ headers: { 'Content-Type': 'application/json' },
606
+ body: JSON.stringify({ text, max_sentences: maxSentences })
607
+ });
608
+
609
+ const data = await response.json();
610
+
611
+ if (!response.ok || !data.ok) {
612
+ throw new Error(data.error || 'Summarization failed');
613
+ }
614
+
615
+ this.displaySummaryResult(resultDiv, data);
616
+ } catch (error) {
617
+ this.showError(resultDiv, error.message);
618
+ } finally {
619
+ btn.disabled = false;
620
+ btn.innerHTML = 'Summarize';
621
+ }
622
+ },
623
+
624
+ displaySummaryResult(container, data) {
625
+ let html = '<div class="result-box">';
626
+ html += '<h3 style="margin-bottom: 15px; color: #f1f5f9;">Summary</h3>';
627
+
628
+ if (data.summary) {
629
+ html += `<div class="summary-text">${this.escapeHtml(data.summary)}</div>`;
630
+ }
631
+
632
+ if (data.sentences && data.sentences.length > 0) {
633
+ html += '<h4 style="margin: 20px 0 10px 0; color: #cbd5e1; font-size: 1.1rem;">Key Sentences</h4>';
634
+ html += '<ul class="sentences-list">';
635
+ data.sentences.forEach(sentence => {
636
+ html += `<li>${this.escapeHtml(sentence)}</li>`;
637
+ });
638
+ html += '</ul>';
639
+ }
640
+
641
+ html += '</div>';
642
+ container.innerHTML = html;
643
+ container.classList.remove('hidden');
644
+ },
645
+
646
+ // Model Status & Diagnostics
647
+ async loadModelStatus() {
648
+ const statusDiv = document.getElementById('registry-status');
649
+ const tableDiv = document.getElementById('models-table');
650
+ const btn = document.getElementById('refresh-status-btn');
651
+
652
+ btn.disabled = true;
653
+ btn.innerHTML = '<span class="loading"></span> Loading...';
654
+
655
+ try {
656
+ const [statusRes, listRes] = await Promise.all([
657
+ fetch('/api/models/status'),
658
+ fetch('/api/models/list')
659
+ ]);
660
+
661
+ const statusData = await statusRes.json();
662
+ const listData = await listRes.json();
663
+
664
+ this.displayRegistryStatus(statusDiv, statusData);
665
+ this.displayModelsTable(tableDiv, listData);
666
+ } catch (error) {
667
+ this.showError(statusDiv, 'Failed to load model status: ' + error.message);
668
+ } finally {
669
+ btn.disabled = false;
670
+ btn.innerHTML = 'Refresh';
671
+ }
672
+ },
673
+
674
+ displayRegistryStatus(container, data) {
675
+ let html = '<div class="status-grid">';
676
+
677
+ html += '<div class="status-item">';
678
+ html += '<div class="status-label">HF Mode</div>';
679
+ html += `<div class="status-value">${data.hf_mode || 'unknown'}</div>`;
680
+ html += '</div>';
681
+
682
+ html += '<div class="status-item">';
683
+ html += '<div class="status-label">Overall Status</div>';
684
+ html += `<div class="status-value">${data.status || 'unknown'}</div>`;
685
+ html += '</div>';
686
+
687
+ html += '<div class="status-item">';
688
+ html += '<div class="status-label">Models Loaded</div>';
689
+ html += `<div class="status-value">${data.models_loaded || 0}</div>`;
690
+ html += '</div>';
691
+
692
+ html += '<div class="status-item">';
693
+ html += '<div class="status-label">Models Failed</div>';
694
+ html += `<div class="status-value">${data.models_failed || 0}</div>`;
695
+ html += '</div>';
696
+
697
+ html += '</div>';
698
+
699
+ if (data.status === 'disabled' || data.hf_mode === 'off') {
700
+ html += '<div class="info-box">';
701
+ html += '<strong>Note:</strong> HF models are disabled. To enable them, set HF_MODE=public or HF_MODE=auth in the environment.';
702
+ html += '</div>';
703
+ } else if (data.models_loaded === 0 && data.status !== 'disabled') {
704
+ html += '<div class="warning-box">';
705
+ html += '<strong>Warning:</strong> No models could be loaded. Check model IDs or HF credentials.';
706
+ html += '</div>';
707
+ }
708
+
709
+ if (data.error) {
710
+ html += '<div class="error-box" style="margin-top: 15px;">';
711
+ html += `<strong>Error:</strong> ${this.escapeHtml(data.error)}`;
712
+ html += '</div>';
713
+ }
714
+
715
+ if (data.failed && data.failed.length > 0) {
716
+ html += '<div style="margin-top: 20px;">';
717
+ html += '<h4 style="color: #cbd5e1; margin-bottom: 10px;">Failed Models</h4>';
718
+ html += '<div style="background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 15px;">';
719
+ data.failed.forEach(([key, error]) => {
720
+ html += `<div style="margin-bottom: 8px; padding: 8px; background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; border-radius: 4px;">`;
721
+ html += `<strong style="color: #fca5a5;">${key}:</strong> `;
722
+ html += `<span style="color: #cbd5e1;">${this.escapeHtml(error)}</span>`;
723
+ html += `</div>`;
724
+ });
725
+ html += '</div>';
726
+ html += '</div>';
727
+ }
728
+
729
+ container.innerHTML = html;
730
+ },
731
+
732
+ displayModelsTable(container, data) {
733
+ if (!data.models || data.models.length === 0) {
734
+ container.innerHTML = '<div class="info-box">No models configured</div>';
735
+ return;
736
+ }
737
+
738
+ let html = '<div class="table-container">';
739
+ html += '<table>';
740
+ html += '<thead><tr>';
741
+ html += '<th>Key</th>';
742
+ html += '<th>Task</th>';
743
+ html += '<th>Model ID</th>';
744
+ html += '<th>Loaded</th>';
745
+ html += '<th>Error</th>';
746
+ html += '</tr></thead>';
747
+ html += '<tbody>';
748
+
749
+ data.models.forEach(model => {
750
+ html += '<tr>';
751
+ html += `<td><strong>${model.key || 'N/A'}</strong></td>`;
752
+ html += `<td>${model.task || 'N/A'}</td>`;
753
+ html += `<td style="font-family: monospace; font-size: 0.85rem;">${model.model_id || 'N/A'}</td>`;
754
+ html += '<td>';
755
+ if (model.loaded) {
756
+ html += '<span class="badge badge-success">Yes</span>';
757
+ } else {
758
+ html += '<span class="badge badge-danger">No</span>';
759
+ }
760
+ html += '</td>';
761
+ html += `<td style="color: #f87171; font-size: 0.85rem;">${model.error ? this.escapeHtml(model.error) : '-'}</td>`;
762
+ html += '</tr>';
763
+ });
764
+
765
+ html += '</tbody>';
766
+ html += '</table>';
767
+ html += '</div>';
768
+
769
+ container.innerHTML = html;
770
+ },
771
+
772
+ // Utility functions
773
+ showError(container, message) {
774
+ container.innerHTML = `<div class="error-box"><strong>Error:</strong> ${this.escapeHtml(message)}</div>`;
775
+ container.classList.remove('hidden');
776
+ },
777
+
778
+ escapeHtml(text) {
779
+ const div = document.createElement('div');
780
+ div.textContent = text;
781
+ return div.innerHTML;
782
+ },
783
+
784
+ // Initialize
785
+ init() {
786
+ document.getElementById('analyze-sentiment-btn').addEventListener('click', () => this.analyzeSentiment());
787
+ document.getElementById('summarize-btn').addEventListener('click', () => this.summarizeText());
788
+ document.getElementById('refresh-status-btn').addEventListener('click', () => this.loadModelStatus());
789
+
790
+ this.loadModelStatus();
791
+ }
792
+ };
793
+
794
+ if (document.readyState === 'loading') {
795
+ document.addEventListener('DOMContentLoaded', () => AITools.init());
796
+ } else {
797
+ AITools.init();
798
+ }
799
+ })();
800
+ </script>
801
+ </body>
802
+ </html>
templates/index.html CHANGED
@@ -1961,6 +1961,10 @@
1961
  <span class="tab-icon"><svg><use href="#icon-reports"></use></svg></span>
1962
  Reports
1963
  </button>
 
 
 
 
1964
  </div>
1965
  </div>
1966
 
 
1961
  <span class="tab-icon"><svg><use href="#icon-reports"></use></svg></span>
1962
  Reports
1963
  </button>
1964
+ <a href="/ai-tools" class="tab" style="text-decoration: none; display: inline-flex; align-items: center; gap: 8px;">
1965
+ <span class="tab-icon"><svg><use href="#icon-hf"></use></svg></span>
1966
+ AI Tools
1967
+ </a>
1968
  </div>
1969
  </div>
1970