Utiric commited on
Commit
aefee54
·
verified ·
1 Parent(s): 4ad9ac6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +660 -2
app.py CHANGED
@@ -200,10 +200,668 @@ def metrics():
200
 
201
  return jsonify(get_performance_metrics())
202
 
203
- if __name__ == '__main__':
204
- # Create directories if they don't exist
 
205
  os.makedirs('templates', exist_ok=True)
206
  os.makedirs('static', exist_ok=True)
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  port = int(os.getenv('PORT', 7860))
209
  app.run(host='0.0.0.0', port=port, debug=True)
 
200
 
201
  return jsonify(get_performance_metrics())
202
 
203
+ # Create a simple file-based approach for the HTML template
204
+ def create_directories_and_files():
205
+ """Create necessary directories and files if they don't exist."""
206
  os.makedirs('templates', exist_ok=True)
207
  os.makedirs('static', exist_ok=True)
208
 
209
+ # Create index.html if it doesn't exist
210
+ index_path = os.path.join('templates', 'index.html')
211
+ if not os.path.exists(index_path):
212
+ with open(index_path, 'w') as f:
213
+ f.write('''<!DOCTYPE html>
214
+ <html lang="en">
215
+ <head>
216
+ <meta charset="UTF-8">
217
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
+ <title>Text Moderation API</title>
219
+ <script src="https://cdn.tailwindcss.com"></script>
220
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
221
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
222
+ <script>
223
+ tailwind.config = {
224
+ darkMode: 'class',
225
+ theme: {
226
+ extend: {
227
+ colors: {
228
+ primary: {
229
+ 50: '#eff6ff',
230
+ 100: '#dbeafe',
231
+ 200: '#bfdbfe',
232
+ 300: '#93c5fd',
233
+ 400: '#60a5fa',
234
+ 500: '#3b82f6',
235
+ 600: '#2563eb',
236
+ 700: '#1d4ed8',
237
+ 800: '#1e40af',
238
+ 900: '#1e3a8a',
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ </script>
245
+ <style>
246
+ .gradient-bg {
247
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
248
+ }
249
+ .dark .gradient-bg {
250
+ background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%);
251
+ }
252
+ .glass-effect {
253
+ background: rgba(255, 255, 255, 0.1);
254
+ backdrop-filter: blur(10px);
255
+ border: 1px solid rgba(255, 255, 255, 0.2);
256
+ }
257
+ .dark .glass-effect {
258
+ background: rgba(30, 41, 59, 0.5);
259
+ border: 1px solid rgba(100, 116, 139, 0.3);
260
+ }
261
+ .category-card {
262
+ transition: all 0.3s ease;
263
+ }
264
+ .category-card:hover {
265
+ transform: translateY(-5px);
266
+ }
267
+ .loading-spinner {
268
+ border-top-color: #3b82f6;
269
+ animation: spinner 1.5s linear infinite;
270
+ }
271
+ @keyframes spinner {
272
+ 0% { transform: rotate(0deg); }
273
+ 100% { transform: rotate(360deg); }
274
+ }
275
+ </style>
276
+ </head>
277
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
278
+ <!-- Header -->
279
+ <header class="gradient-bg text-white shadow-lg">
280
+ <div class="container mx-auto px-4 py-6 flex justify-between items-center">
281
+ <div class="flex items-center space-x-3">
282
+ <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center">
283
+ <i class="fas fa-shield-alt text-primary-600 text-xl"></i>
284
+ </div>
285
+ <h1 class="text-2xl font-bold">Text Moderation API</h1>
286
+ </div>
287
+ <div class="flex items-center space-x-4">
288
+ <button id="refreshMetrics" class="glass-effect px-4 py-2 rounded-lg hover:bg-white/20 transition">
289
+ <i class="fas fa-sync-alt mr-2"></i>Refresh Metrics
290
+ </button>
291
+ <button id="darkModeToggle" class="glass-effect p-2 rounded-lg hover:bg-white/20 transition">
292
+ <i class="fas fa-moon dark:hidden"></i>
293
+ <i class="fas fa-sun hidden dark:inline"></i>
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </header>
298
+
299
+ <main class="container mx-auto px-4 py-8">
300
+ <!-- Performance Metrics Section -->
301
+ <section class="mb-12">
302
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
303
+ <i class="fas fa-chart-line mr-3 text-primary-600"></i>
304
+ Performance Metrics
305
+ </h2>
306
+
307
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
308
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
309
+ <div class="flex items-center justify-between">
310
+ <div>
311
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response Time</p>
312
+ <p class="text-2xl font-bold" id="avgResponseTime">0ms</p>
313
+ </div>
314
+ <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
315
+ <i class="fas fa-clock text-primary-600 dark:text-primary-400"></i>
316
+ </div>
317
+ </div>
318
+ </div>
319
+
320
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
321
+ <div class="flex items-center justify-between">
322
+ <div>
323
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Requests/Second</p>
324
+ <p class="text-2xl font-bold" id="requestsPerSecond">0</p>
325
+ </div>
326
+ <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
327
+ <i class="fas fa-tachometer-alt text-primary-600 dark:text-primary-400"></i>
328
+ </div>
329
+ </div>
330
+ </div>
331
+
332
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
333
+ <div class="flex items-center justify-between">
334
+ <div>
335
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Concurrent Requests</p>
336
+ <p class="text-2xl font-bold" id="concurrentRequests">0</p>
337
+ </div>
338
+ <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
339
+ <i class="fas fa-network-wired text-primary-600 dark:text-primary-400"></i>
340
+ </div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
345
+ <div class="flex items-center justify-between">
346
+ <div>
347
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Today's Tokens</p>
348
+ <p class="text-2xl font-bold" id="todayTokens">0</p>
349
+ </div>
350
+ <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
351
+ <i class="fas fa-key text-primary-600 dark:text-primary-400"></i>
352
+ </div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
358
+ <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3>
359
+ <div class="h-64">
360
+ <canvas id="activityChart"></canvas>
361
+ </div>
362
+ </div>
363
+ </section>
364
+
365
+ <!-- API Testing Section -->
366
+ <section class="mb-12">
367
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
368
+ <i class="fas fa-code mr-3 text-primary-600"></i>
369
+ API Tester
370
+ </h2>
371
+
372
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
373
+ <form id="apiTestForm">
374
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
375
+ <div>
376
+ <label class="block text-sm font-medium mb-2" for="apiKey">API Key</label>
377
+ <input type="password" id="apiKey" class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="Enter your API key">
378
+ </div>
379
+
380
+ <div>
381
+ <label class="block text-sm font-medium mb-2" for="model">Model</label>
382
+ <select id="model" class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500">
383
+ <option value="unitaryai/detoxify-multilingual" selected>Detoxify Multilingual</option>
384
+ </select>
385
+ </div>
386
+ </div>
387
+
388
+ <div class="mb-6">
389
+ <label class="block text-sm font-medium mb-2">Text Inputs</label>
390
+ <div id="textInputsContainer">
391
+ <div class="text-input-group mb-4">
392
+ <textarea class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" rows="3" placeholder="Enter text to moderate..."></textarea>
393
+ <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700 hidden">
394
+ <i class="fas fa-trash-alt mr-1"></i> Remove
395
+ </button>
396
+ </div>
397
+ </div>
398
+ <button type="button" id="addTextInput" class="mt-2 text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300">
399
+ <i class="fas fa-plus-circle mr-1"></i> Add another text input
400
+ </button>
401
+ </div>
402
+
403
+ <div class="flex justify-between items-center">
404
+ <div>
405
+ <button type="submit" id="analyzeBtn" class="bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-6 rounded-lg transition">
406
+ <i class="fas fa-search mr-2"></i> Analyze Text
407
+ </button>
408
+ <button type="button" id="clearBtn" class="ml-2 bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium py-2 px-6 rounded-lg transition">
409
+ <i class="fas fa-eraser mr-2"></i> Clear
410
+ </button>
411
+ </div>
412
+ <div class="text-sm text-gray-500 dark:text-gray-400">
413
+ <i class="fas fa-info-circle mr-1"></i> Maximum 10 text inputs allowed
414
+ </div>
415
+ </div>
416
+ </form>
417
+ </div>
418
+ </section>
419
+
420
+ <!-- Results Section -->
421
+ <section id="resultsSection" class="hidden">
422
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
423
+ <i class="fas fa-clipboard-check mr-3 text-primary-600"></i>
424
+ Analysis Results
425
+ </h2>
426
+
427
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 mb-6">
428
+ <div class="flex justify-between items-center mb-4">
429
+ <h3 class="text-lg font-semibold">Summary</h3>
430
+ <div class="text-sm text-gray-500 dark:text-gray-400">
431
+ <i class="fas fa-clock mr-1"></i> Response time: <span id="responseTime">0ms</span> |
432
+ <i class="fas fa-key ml-2 mr-1"></i> Tokens: <span id="tokenCount">0</span>
433
+ </div>
434
+ </div>
435
+
436
+ <div id="resultsContainer" class="space-y-6">
437
+ <!-- Results will be dynamically inserted here -->
438
+ </div>
439
+ </div>
440
+ </section>
441
+
442
+ <!-- API Documentation Section -->
443
+ <section>
444
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
445
+ <i class="fas fa-book mr-3 text-primary-600"></i>
446
+ API Documentation
447
+ </h2>
448
+
449
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
450
+ <h3 class="text-lg font-semibold mb-4">Endpoint</h3>
451
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">
452
+ POST /v1/moderations
453
+ </div>
454
+
455
+ <h3 class="text-lg font-semibold mb-4">Request Body</h3>
456
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 overflow-x-auto">
457
+ <pre class="text-sm"><code>{
458
+ "model": "unitaryai/detoxify-multilingual",
459
+ "input": "Text to moderate"
460
+ }</code></pre>
461
+ </div>
462
+
463
+ <h3 class="text-lg font-semibold mb-4">Response</h3>
464
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
465
+ <pre class="text-sm"><code>{
466
+ "id": "modr-1234567890abcdef",
467
+ "model": "unitaryai/detoxify-multilingual",
468
+ "results": [
469
+ {
470
+ "flagged": true,
471
+ "categories": {
472
+ "toxicity": true,
473
+ "severe_toxicity": false,
474
+ "obscene": true,
475
+ "threat": false,
476
+ "insult": true,
477
+ "identity_attack": false,
478
+ "sexual_explicit": false
479
+ },
480
+ "category_scores": {
481
+ "toxicity": 0.95,
482
+ "severe_toxicity": 0.1,
483
+ "obscene": 0.8,
484
+ "threat": 0.05,
485
+ "insult": 0.7,
486
+ "identity_attack": 0.2,
487
+ "sexual_explicit": 0.01
488
+ },
489
+ "category_applied_input_types": {
490
+ "toxicity": ["text"],
491
+ "severe_toxicity": [],
492
+ "obscene": ["text"],
493
+ "threat": [],
494
+ "insult": ["text"],
495
+ "identity_attack": [],
496
+ "sexual_explicit": []
497
+ }
498
+ }
499
+ ],
500
+ "object": "moderation",
501
+ "usage": {
502
+ "total_tokens": 5
503
+ }
504
+ }</code></pre>
505
+ </div>
506
+ </div>
507
+ </section>
508
+ </main>
509
+
510
+ <!-- Footer -->
511
+ <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
512
+ <div class="container mx-auto px-4 py-6">
513
+ <div class="flex flex-col md:flex-row justify-between items-center">
514
+ <div class="mb-4 md:mb-0">
515
+ <p class="text-gray-600 dark:text-gray-400">© 2025 Text Moderation API. All rights reserved.</p>
516
+ </div>
517
+ <div class="flex space-x-4">
518
+ <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
519
+ <i class="fab fa-github"></i>
520
+ </a>
521
+ <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
522
+ <i class="fab fa-twitter"></i>
523
+ </a>
524
+ <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
525
+ <i class="fas fa-envelope"></i>
526
+ </a>
527
+ </div>
528
+ </div>
529
+ </div>
530
+ </footer>
531
+
532
+ <script>
533
+ // Dark mode toggle
534
+ const darkModeToggle = document.getElementById('darkModeToggle');
535
+ const html = document.documentElement;
536
+
537
+ // Check for saved dark mode preference or default to light mode
538
+ const currentTheme = localStorage.getItem('theme') || 'light';
539
+ if (currentTheme === 'dark') {
540
+ html.classList.add('dark');
541
+ }
542
+
543
+ darkModeToggle.addEventListener('click', () => {
544
+ html.classList.toggle('dark');
545
+ const theme = html.classList.contains('dark') ? 'dark' : 'light';
546
+ localStorage.setItem('theme', theme);
547
+ });
548
+
549
+ // Chart for activity
550
+ let activityChart;
551
+
552
+ function initActivityChart() {
553
+ const ctx = document.getElementById('activityChart').getContext('2d');
554
+ activityChart = new Chart(ctx, {
555
+ type: 'bar',
556
+ data: {
557
+ labels: [],
558
+ datasets: [
559
+ {
560
+ label: 'Requests',
561
+ data: [],
562
+ backgroundColor: 'rgba(59, 130, 246, 0.5)',
563
+ borderColor: 'rgba(59, 130, 246, 1)',
564
+ borderWidth: 1
565
+ },
566
+ {
567
+ label: 'Tokens',
568
+ data: [],
569
+ backgroundColor: 'rgba(16, 185, 129, 0.5)',
570
+ borderColor: 'rgba(16, 185, 129, 1)',
571
+ borderWidth: 1,
572
+ yAxisID: 'y1'
573
+ }
574
+ ]
575
+ },
576
+ options: {
577
+ responsive: true,
578
+ maintainAspectRatio: false,
579
+ scales: {
580
+ y: {
581
+ beginAtZero: true,
582
+ position: 'left',
583
+ title: {
584
+ display: true,
585
+ text: 'Requests'
586
+ }
587
+ },
588
+ y1: {
589
+ beginAtZero: true,
590
+ position: 'right',
591
+ title: {
592
+ display: true,
593
+ text: 'Tokens'
594
+ },
595
+ grid: {
596
+ drawOnChartArea: false
597
+ }
598
+ }
599
+ }
600
+ }
601
+ });
602
+ }
603
+
604
+ // Fetch performance metrics
605
+ async function fetchMetrics() {
606
+ const apiKey = document.getElementById('apiKey').value;
607
+ if (!apiKey) {
608
+ console.error('API key is required');
609
+ return;
610
+ }
611
+
612
+ try {
613
+ const response = await fetch('/v1/metrics', {
614
+ method: 'GET',
615
+ headers: {
616
+ 'Content-Type': 'application/json',
617
+ 'Authorization': 'Bearer ' + apiKey
618
+ }
619
+ });
620
+
621
+ if (!response.ok) {
622
+ throw new Error('Failed to fetch metrics');
623
+ }
624
+
625
+ const data = await response.json();
626
+ updateMetricsDisplay(data);
627
+ } catch (error) {
628
+ console.error('Error fetching metrics:', error);
629
+ }
630
+ }
631
+
632
+ // Update metrics display
633
+ function updateMetricsDisplay(data) {
634
+ document.getElementById('avgResponseTime').textContent = (data.avg_request_time * 1000).toFixed(2) + 'ms';
635
+ document.getElementById('requestsPerSecond').textContent = data.requests_per_second.toFixed(2);
636
+ document.getElementById('concurrentRequests').textContent = data.concurrent_requests;
637
+ document.getElementById('todayTokens').textContent = data.today_tokens.toLocaleString();
638
+
639
+ // Update activity chart
640
+ if (activityChart) {
641
+ const labels = data.last_7_days.map(day => {
642
+ const date = new Date(day.date);
643
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
644
+ }).reverse();
645
+
646
+ const requests = data.last_7_days.map(day => day.requests).reverse();
647
+ const tokens = data.last_7_days.map(day => day.tokens).reverse();
648
+
649
+ activityChart.data.labels = labels;
650
+ activityChart.data.datasets[0].data = requests;
651
+ activityChart.data.datasets[1].data = tokens;
652
+ activityChart.update();
653
+ }
654
+ }
655
+
656
+ // Add text input
657
+ document.getElementById('addTextInput').addEventListener('click', () => {
658
+ const container = document.getElementById('textInputsContainer');
659
+ const inputGroups = container.querySelectorAll('.text-input-group');
660
+
661
+ if (inputGroups.length >= 10) {
662
+ alert('Maximum 10 text inputs allowed');
663
+ return;
664
+ }
665
+
666
+ const newInputGroup = document.createElement('div');
667
+ newInputGroup.className = 'text-input-group mb-4';
668
+ newInputGroup.innerHTML = `
669
+ <textarea class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" rows="3" placeholder="Enter text to moderate..."></textarea>
670
+ <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700">
671
+ <i class="fas fa-trash-alt mr-1"></i> Remove
672
+ </button>
673
+ `;
674
+
675
+ container.appendChild(newInputGroup);
676
+
677
+ // Add event listener to remove button
678
+ newInputGroup.querySelector('.remove-input').addEventListener('click', function() {
679
+ newInputGroup.remove();
680
+ updateRemoveButtons();
681
+ });
682
+
683
+ updateRemoveButtons();
684
+ });
685
+
686
+ // Update remove buttons visibility
687
+ function updateRemoveButtons() {
688
+ const inputGroups = document.querySelectorAll('.text-input-group');
689
+ inputGroups.forEach((group, index) => {
690
+ const removeBtn = group.querySelector('.remove-input');
691
+ if (inputGroups.length > 1) {
692
+ removeBtn.classList.remove('hidden');
693
+ } else {
694
+ removeBtn.classList.add('hidden');
695
+ }
696
+ });
697
+ }
698
+
699
+ // Remove text input
700
+ document.addEventListener('click', function(e) {
701
+ if (e.target.closest('.remove-input')) {
702
+ e.target.closest('.text-input-group').remove();
703
+ updateRemoveButtons();
704
+ }
705
+ });
706
+
707
+ // Clear form
708
+ document.getElementById('clearBtn').addEventListener('click', () => {
709
+ document.getElementById('apiTestForm').reset();
710
+ const container = document.getElementById('textInputsContainer');
711
+ container.innerHTML = `
712
+ <div class="text-input-group mb-4">
713
+ <textarea class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500" rows="3" placeholder="Enter text to moderate..."></textarea>
714
+ <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700 hidden">
715
+ <i class="fas fa-trash-alt mr-1"></i> Remove
716
+ </button>
717
+ </div>
718
+ `;
719
+ document.getElementById('resultsSection').classList.add('hidden');
720
+ });
721
+
722
+ // Analyze text
723
+ document.getElementById('apiTestForm').addEventListener('submit', async (e) => {
724
+ e.preventDefault();
725
+
726
+ const apiKey = document.getElementById('apiKey').value;
727
+ const model = document.getElementById('model').value;
728
+ const textInputs = document.querySelectorAll('#textInputsContainer textarea');
729
+
730
+ if (!apiKey) {
731
+ alert('Please enter your API key');
732
+ return;
733
+ }
734
+
735
+ const texts = [];
736
+ textInputs.forEach(input => {
737
+ if (input.value.trim()) {
738
+ texts.push(input.value.trim());
739
+ }
740
+ });
741
+
742
+ if (texts.length === 0) {
743
+ alert('Please enter at least one text to analyze');
744
+ return;
745
+ }
746
+
747
+ const analyzeBtn = document.getElementById('analyzeBtn');
748
+ const originalBtnContent = analyzeBtn.innerHTML;
749
+ analyzeBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"></div> Analyzing...';
750
+ analyzeBtn.disabled = true;
751
+
752
+ const startTime = Date.now();
753
+
754
+ try {
755
+ const response = await fetch('/v1/moderations', {
756
+ method: 'POST',
757
+ headers: {
758
+ 'Content-Type': 'application/json',
759
+ 'Authorization': 'Bearer ' + apiKey
760
+ },
761
+ body: JSON.stringify({
762
+ model: model,
763
+ input: texts
764
+ })
765
+ });
766
+
767
+ const endTime = Date.now();
768
+ const responseTime = endTime - startTime;
769
+
770
+ if (!response.ok) {
771
+ const errorData = await response.json();
772
+ throw new Error(errorData.error || 'Failed to analyze text');
773
+ }
774
+
775
+ const data = await response.json();
776
+ displayResults(data, responseTime);
777
+
778
+ } catch (error) {
779
+ console.error('Error analyzing text:', error);
780
+ alert('Error: ' + error.message);
781
+ } finally {
782
+ analyzeBtn.innerHTML = originalBtnContent;
783
+ analyzeBtn.disabled = false;
784
+ }
785
+ });
786
+
787
+ // Display results
788
+ function displayResults(data, responseTime) {
789
+ const resultsSection = document.getElementById('resultsSection');
790
+ const resultsContainer = document.getElementById('resultsContainer');
791
+
792
+ document.getElementById('responseTime').textContent = responseTime + 'ms';
793
+ document.getElementById('tokenCount').textContent = data.usage.total_tokens.toLocaleString();
794
+
795
+ resultsContainer.innerHTML = '';
796
+
797
+ data.results.forEach((result, index) => {
798
+ const resultCard = document.createElement('div');
799
+ resultCard.className = 'border border-gray-200 dark:border-gray-700 rounded-lg p-4';
800
+
801
+ const flaggedBadge = result.flagged
802
+ ? '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100"><i class="fas fa-exclamation-triangle mr-1"></i> Flagged</span>'
803
+ : '<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100"><i class="fas fa-check-circle mr-1"></i> Safe</span>';
804
+
805
+ let categoriesHtml = '';
806
+ for (const [category, isFlagged] of Object.entries(result.categories)) {
807
+ const score = result.category_scores[category];
808
+ const categoryClass = isFlagged ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400';
809
+ const scoreClass = score > 0.7 ? 'text-red-600 dark:text-red-400' : score > 0.4 ? 'text-yellow-600 dark:text-yellow-400' : 'text-green-600 dark:text-green-400';
810
+
811
+ categoriesHtml += `
812
+ <div class="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
813
+ <span class="font-medium capitalize">${category.replace('_', ' ')}</span>
814
+ <div class="flex items-center">
815
+ <span class="${categoryClass} mr-2">${isFlagged ? 'Flagged' : 'Safe'}</span>
816
+ <span class="text-sm ${scoreClass} font-mono">${score.toFixed(4)}</span>
817
+ </div>
818
+ </div>
819
+ `;
820
+ }
821
+
822
+ resultCard.innerHTML = `
823
+ <div class="flex justify-between items-start mb-3">
824
+ <h4 class="text-lg font-semibold">Text ${index + 1}</h4>
825
+ ${flaggedBadge}
826
+ </div>
827
+ <div class="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg text-sm">
828
+ ${texts[index]}
829
+ </div>
830
+ <div class="category-card">
831
+ <h5 class="font-medium mb-2">Categories</h5>
832
+ <div class="bg-white dark:bg-gray-700 rounded-lg overflow-hidden">
833
+ ${categoriesHtml}
834
+ </div>
835
+ </div>
836
+ `;
837
+
838
+ resultsContainer.appendChild(resultCard);
839
+ });
840
+
841
+ resultsSection.classList.remove('hidden');
842
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
843
+ }
844
+
845
+ // Initialize chart on page load
846
+ document.addEventListener('DOMContentLoaded', () => {
847
+ initActivityChart();
848
+
849
+ // Set up refresh metrics button
850
+ document.getElementById('refreshMetrics').addEventListener('click', fetchMetrics);
851
+
852
+ // Initial metrics fetch
853
+ fetchMetrics();
854
+
855
+ // Auto-refresh metrics every 30 seconds
856
+ setInterval(fetchMetrics, 30000);
857
+ });
858
+ </script>
859
+ </body>
860
+ </html>''')
861
+
862
+ if __name__ == '__main__':
863
+ # Create directories and files
864
+ create_directories_and_files()
865
+
866
  port = int(os.getenv('PORT', 7860))
867
  app.run(host='0.0.0.0', port=port, debug=True)