Utiric commited on
Commit
bb1a9b2
·
verified ·
1 Parent(s): a2f40a7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -314
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, request, jsonify, render_template, send_from_directory
2
  import os
3
  import uuid
4
  import time
@@ -10,11 +10,18 @@ from detoxify import Detoxify
10
 
11
  app = Flask(__name__, static_folder='static', template_folder='templates')
12
 
 
13
  detoxify_model = Detoxify('multilingual')
 
14
 
15
  API_KEY = os.getenv('API_KEY', 'your-api-key-here')
16
 
17
- request_times = deque(maxlen=1000)
 
 
 
 
 
18
  daily_requests = defaultdict(int)
19
  daily_tokens = defaultdict(int)
20
  concurrent_requests = 0
@@ -33,7 +40,8 @@ def transform_predictions(prediction_dict):
33
 
34
  scores = {}
35
  for key in category_keys:
36
- scores[key] = prediction_dict.get(key, 0.0)
 
37
 
38
  threshold = 0.5
39
  bool_categories = {key: (scores[key] > threshold) for key in category_keys}
@@ -44,7 +52,10 @@ def transform_predictions(prediction_dict):
44
 
45
  def track_request_metrics(start_time, tokens_count):
46
  end_time = time.time()
47
- request_times.append(end_time - start_time)
 
 
 
48
 
49
  today = datetime.now().strftime("%Y-%m-%d")
50
  daily_requests[today] += 1
@@ -55,19 +66,26 @@ def get_performance_metrics():
55
  with concurrent_requests_lock:
56
  current_concurrent = concurrent_requests
57
 
58
- avg_request_time = sum(request_times) / len(request_times) if request_times else 0
59
-
60
- today = datetime.now().strftime("%Y-%m-%d")
61
-
62
- recent_requests = list(request_times)[-100:] if len(request_times) >= 100 else list(request_times)
63
- requests_per_second = len(recent_requests) / sum(recent_requests) if recent_requests and sum(recent_requests) > 0 else 0
 
 
 
 
 
 
64
 
 
65
  today_requests = daily_requests.get(today, 0)
66
  today_tokens = daily_tokens.get(today, 0)
67
 
68
  last_7_days = []
69
  for i in range(7):
70
- date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
71
  last_7_days.append({
72
  "date": date,
73
  "requests": daily_requests.get(date, 0),
@@ -75,8 +93,9 @@ def get_performance_metrics():
75
  })
76
 
77
  return {
78
- "avg_request_time": avg_request_time,
79
- "requests_per_second": requests_per_second,
 
80
  "concurrent_requests": current_concurrent,
81
  "today_requests": today_requests,
82
  "today_tokens": today_tokens,
@@ -107,10 +126,10 @@ def moderations():
107
  return jsonify({"error": "Unauthorized"}), 401
108
 
109
  data = request.get_json()
110
- raw_input = data.get('input') or data.get('texts')
111
 
112
  if raw_input is None:
113
- return jsonify({"error": "Invalid input, expected 'input' or 'texts' field"}), 400
114
 
115
  if isinstance(raw_input, str):
116
  texts = [raw_input]
@@ -119,41 +138,44 @@ def moderations():
119
  else:
120
  return jsonify({"error": "Invalid input format, expected string or list of strings"}), 400
121
 
 
 
 
122
  if len(texts) > 10:
123
  return jsonify({"error": "Too many input items. Maximum 10 allowed."}), 400
124
 
125
  for text in texts:
126
- if not isinstance(text, str) or len(text) > 100000:
127
- return jsonify({"error": "Each input item must be a string with a maximum of 100k characters."}), 400
128
  total_tokens += count_tokens(text)
129
 
130
  results = []
131
- for text in texts:
132
- pred = detoxify_model.predict([text])
133
- prediction = {k: v[0] for k, v in pred.items()}
134
- flagged, bool_categories, scores, cat_applied_input_types = transform_predictions(prediction)
 
 
135
 
136
  results.append({
137
  "flagged": flagged,
138
  "categories": bool_categories,
139
  "category_scores": scores,
140
- "category_applied_input_types": cat_applied_input_types
141
  })
142
 
143
  track_request_metrics(start_time, total_tokens)
144
 
145
  response_data = {
146
  "id": "modr-" + uuid.uuid4().hex[:24],
147
- "model": "unitaryai/detoxify-multilingual",
148
- "results": results,
149
- "object": "moderation",
150
- "usage": {
151
- "total_tokens": total_tokens
152
- }
153
  }
154
 
155
  return jsonify(response_data)
156
 
 
 
 
157
  finally:
158
  with concurrent_requests_lock:
159
  concurrent_requests -= 1
@@ -176,7 +198,7 @@ def create_directories_and_files():
176
 
177
  index_path = os.path.join('templates', 'index.html')
178
  if not os.path.exists(index_path):
179
- with open(index_path, 'w') as f:
180
  f.write('''<!DOCTYPE html>
181
  <html lang="en">
182
  <head>
@@ -193,16 +215,8 @@ def create_directories_and_files():
193
  extend: {
194
  colors: {
195
  primary: {
196
- 50: '#eff6ff',
197
- 100: '#dbeafe',
198
- 200: '#bfdbfe',
199
- 300: '#93c5fd',
200
- 400: '#60a5fa',
201
- 500: '#3b82f6',
202
- 600: '#2563eb',
203
- 700: '#1d4ed8',
204
- 800: '#1e40af',
205
- 900: '#1e3a8a',
206
  }
207
  }
208
  }
@@ -210,38 +224,20 @@ def create_directories_and_files():
210
  }
211
  </script>
212
  <style>
213
- .gradient-bg {
214
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
215
- }
216
- .dark .gradient-bg {
217
- background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%);
218
- }
219
  .glass-effect {
220
- background: rgba(255, 255, 255, 0.1);
221
- backdrop-filter: blur(10px);
222
  border: 1px solid rgba(255, 255, 255, 0.2);
223
  }
224
- .dark .glass-effect {
225
- background: rgba(30, 41, 59, 0.5);
226
- border: 1px solid rgba(100, 116, 139, 0.3);
227
- }
228
- .category-card {
229
- transition: all 0.3s ease;
230
- }
231
- .category-card:hover {
232
- transform: translateY(-5px);
233
- }
234
- .loading-spinner {
235
- border-top-color: #3b82f6;
236
- animation: spinner 1.5s linear infinite;
237
- }
238
- @keyframes spinner {
239
- 0% { transform: rotate(0deg); }
240
- 100% { transform: rotate(360deg); }
241
- }
242
  </style>
243
  </head>
244
- <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
245
  <header class="gradient-bg text-white shadow-lg">
246
  <div class="container mx-auto px-4 py-6 flex justify-between items-center">
247
  <div class="flex items-center space-x-3">
@@ -270,10 +266,11 @@ def create_directories_and_files():
270
  </h2>
271
 
272
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
 
273
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
274
  <div class="flex items-center justify-between">
275
  <div>
276
- <p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response Time</p>
277
  <p class="text-2xl font-bold" id="avgResponseTime">0ms</p>
278
  </div>
279
  <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
@@ -282,38 +279,41 @@ def create_directories_and_files():
282
  </div>
283
  </div>
284
 
 
285
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
286
  <div class="flex items-center justify-between">
287
  <div>
288
- <p class="text-gray-500 dark:text-gray-400 text-sm">Requests/Second</p>
289
- <p class="text-2xl font-bold" id="requestsPerSecond">0</p>
290
  </div>
291
- <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
292
- <i class="fas fa-tachometer-alt text-primary-600 dark:text-primary-400"></i>
293
  </div>
294
  </div>
295
  </div>
296
 
 
297
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
298
  <div class="flex items-center justify-between">
299
  <div>
300
- <p class="text-gray-500 dark:text-gray-400 text-sm">Concurrent Requests</p>
301
- <p class="text-2xl font-bold" id="concurrentRequests">0</p>
302
  </div>
303
- <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
304
- <i class="fas fa-network-wired text-primary-600 dark:text-primary-400"></i>
305
  </div>
306
  </div>
307
  </div>
308
 
 
309
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
310
  <div class="flex items-center justify-between">
311
  <div>
312
- <p class="text-gray-500 dark:text-gray-400 text-sm">Today's Tokens</p>
313
- <p class="text-2xl font-bold" id="todayTokens">0</p>
314
  </div>
315
- <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
316
- <i class="fas fa-key text-primary-600 dark:text-primary-400"></i>
317
  </div>
318
  </div>
319
  </div>
@@ -327,6 +327,7 @@ def create_directories_and_files():
327
  </div>
328
  </section>
329
 
 
330
  <section class="mb-12">
331
  <h2 class="text-2xl font-bold mb-6 flex items-center">
332
  <i class="fas fa-code mr-3 text-primary-600"></i>
@@ -335,18 +336,9 @@ def create_directories_and_files():
335
 
336
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
337
  <form id="apiTestForm">
338
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
339
- <div>
340
- <label class="block text-sm font-medium mb-2" for="apiKey">API Key</label>
341
- <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">
342
- </div>
343
-
344
- <div>
345
- <label class="block text-sm font-medium mb-2" for="model">Model</label>
346
- <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">
347
- <option value="unitaryai/detoxify-multilingual" selected>Detoxify Multilingual</option>
348
- </select>
349
- </div>
350
  </div>
351
 
352
  <div class="mb-6">
@@ -381,6 +373,7 @@ def create_directories_and_files():
381
  </div>
382
  </section>
383
 
 
384
  <section id="resultsSection" class="hidden">
385
  <h2 class="text-2xl font-bold mb-6 flex items-center">
386
  <i class="fas fa-clipboard-check mr-3 text-primary-600"></i>
@@ -391,8 +384,7 @@ def create_directories_and_files():
391
  <div class="flex justify-between items-center mb-4">
392
  <h3 class="text-lg font-semibold">Summary</h3>
393
  <div class="text-sm text-gray-500 dark:text-gray-400">
394
- <i class="fas fa-clock mr-1"></i> Response time: <span id="responseTime">0ms</span> |
395
- <i class="fas fa-key ml-2 mr-1"></i> Tokens: <span id="tokenCount">0</span>
396
  </div>
397
  </div>
398
 
@@ -401,67 +393,41 @@ def create_directories_and_files():
401
  </div>
402
  </section>
403
 
 
404
  <section>
405
  <h2 class="text-2xl font-bold mb-6 flex items-center">
406
  <i class="fas fa-book mr-3 text-primary-600"></i>
407
  API Documentation
408
  </h2>
409
-
410
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
411
  <h3 class="text-lg font-semibold mb-4">Endpoint</h3>
412
- <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">
413
- POST /v1/moderations
414
- </div>
415
-
416
  <h3 class="text-lg font-semibold mb-4">Request Body</h3>
417
  <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 overflow-x-auto">
418
  <pre class="text-sm"><code>{
419
- "model": "unitaryai/detoxify-multilingual",
420
  "input": "Text to moderate"
421
  }</code></pre>
422
  </div>
423
-
424
  <h3 class="text-lg font-semibold mb-4">Response</h3>
425
  <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
426
  <pre class="text-sm"><code>{
427
  "id": "modr-1234567890abcdef",
428
- "model": "unitaryai/detoxify-multilingual",
429
  "results": [
430
  {
431
  "flagged": true,
432
  "categories": {
433
  "toxicity": true,
434
  "severe_toxicity": false,
435
- "obscene": true,
436
- "threat": false,
437
- "insult": true,
438
- "identity_attack": false,
439
- "sexual_explicit": false
440
  },
441
  "category_scores": {
442
  "toxicity": 0.95,
443
  "severe_toxicity": 0.1,
444
- "obscene": 0.8,
445
- "threat": 0.05,
446
- "insult": 0.7,
447
- "identity_attack": 0.2,
448
- "sexual_explicit": 0.01
449
- },
450
- "category_applied_input_types": {
451
- "toxicity": ["text"],
452
- "severe_toxicity": [],
453
- "obscene": ["text"],
454
- "threat": [],
455
- "insult": ["text"],
456
- "identity_attack": [],
457
- "sexual_explicit": []
458
  }
459
  }
460
- ],
461
- "object": "moderation",
462
- "usage": {
463
- "total_tokens": 5
464
- }
465
  }</code></pre>
466
  </div>
467
  </div>
@@ -469,23 +435,8 @@ def create_directories_and_files():
469
  </main>
470
 
471
  <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
472
- <div class="container mx-auto px-4 py-6">
473
- <div class="flex flex-col md:flex-row justify-between items-center">
474
- <div class="mb-4 md:mb-0">
475
- <p class="text-gray-600 dark:text-gray-400">© 2025 Text Moderation API. All rights reserved.</p>
476
- </div>
477
- <div class="flex space-x-4">
478
- <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
479
- <i class="fab fa-github"></i>
480
- </a>
481
- <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
482
- <i class="fab fa-twitter"></i>
483
- </a>
484
- <a href="#" class="text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400">
485
- <i class="fas fa-envelope"></i>
486
- </a>
487
- </div>
488
- </div>
489
  </div>
490
  </footer>
491
 
@@ -493,91 +444,55 @@ def create_directories_and_files():
493
  const darkModeToggle = document.getElementById('darkModeToggle');
494
  const html = document.documentElement;
495
 
496
- const currentTheme = localStorage.getItem('theme') || 'light';
497
- if (currentTheme === 'dark') {
498
  html.classList.add('dark');
499
  }
500
 
501
  darkModeToggle.addEventListener('click', () => {
502
  html.classList.toggle('dark');
503
- const theme = html.classList.contains('dark') ? 'dark' : 'light';
504
- localStorage.setItem('theme', theme);
505
  });
506
 
507
  let activityChart;
508
 
509
  function initActivityChart() {
 
510
  const ctx = document.getElementById('activityChart').getContext('2d');
 
 
 
 
511
  activityChart = new Chart(ctx, {
512
  type: 'bar',
513
- data: {
514
- labels: [],
515
- datasets: [
516
- {
517
- label: 'Requests',
518
- data: [],
519
- backgroundColor: 'rgba(59, 130, 246, 0.5)',
520
- borderColor: 'rgba(59, 130, 246, 1)',
521
- borderWidth: 1
522
- },
523
- {
524
- label: 'Tokens',
525
- data: [],
526
- backgroundColor: 'rgba(16, 185, 129, 0.5)',
527
- borderColor: 'rgba(16, 185, 129, 1)',
528
- borderWidth: 1,
529
- yAxisID: 'y1'
530
- }
531
- ]
532
- },
533
  options: {
534
- responsive: true,
535
- maintainAspectRatio: false,
536
  scales: {
537
- y: {
538
- beginAtZero: true,
539
- position: 'left',
540
- title: {
541
- display: true,
542
- text: 'Requests'
543
- }
544
- },
545
- y1: {
546
- beginAtZero: true,
547
- position: 'right',
548
- title: {
549
- display: true,
550
- text: 'Tokens'
551
- },
552
- grid: {
553
- drawOnChartArea: false
554
- }
555
- }
556
- }
557
  }
558
  });
559
  }
560
 
561
  async function fetchMetrics() {
562
- const apiKey = document.getElementById('apiKey').value;
563
- if (!apiKey) {
564
- console.error('API key is required');
565
- return;
566
- }
567
-
568
  try {
569
  const response = await fetch('/v1/metrics', {
570
- method: 'GET',
571
- headers: {
572
- 'Content-Type': 'application/json',
573
- 'Authorization': 'Bearer ' + apiKey
574
- }
575
  });
576
-
577
  if (!response.ok) {
578
- throw new Error('Failed to fetch metrics');
 
 
 
 
 
579
  }
580
-
581
  const data = await response.json();
582
  updateMetricsDisplay(data);
583
  } catch (error) {
@@ -586,19 +501,15 @@ def create_directories_and_files():
586
  }
587
 
588
  function updateMetricsDisplay(data) {
589
- document.getElementById('avgResponseTime').textContent = (data.avg_request_time * 1000).toFixed(2) + 'ms';
590
- document.getElementById('requestsPerSecond').textContent = data.requests_per_second.toFixed(2);
591
- document.getElementById('concurrentRequests').textContent = data.concurrent_requests;
592
- document.getElementById('todayTokens').textContent = data.today_tokens.toLocaleString();
593
 
594
  if (activityChart) {
595
- const labels = data.last_7_days.map(day => {
596
- const date = new Date(day.date);
597
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
598
- }).reverse();
599
-
600
- const requests = data.last_7_days.map(day => day.requests).reverse();
601
- const tokens = data.last_7_days.map(day => day.tokens).reverse();
602
 
603
  activityChart.data.labels = labels;
604
  activityChart.data.datasets[0].data = requests;
@@ -606,65 +517,40 @@ def create_directories_and_files():
606
  activityChart.update();
607
  }
608
  }
609
-
610
  document.getElementById('addTextInput').addEventListener('click', () => {
611
  const container = document.getElementById('textInputsContainer');
612
- const inputGroups = container.querySelectorAll('.text-input-group');
613
-
614
- if (inputGroups.length >= 10) {
615
  alert('Maximum 10 text inputs allowed');
616
  return;
617
  }
618
-
619
- const newInputGroup = document.createElement('div');
620
- newInputGroup.className = 'text-input-group mb-4';
621
- newInputGroup.innerHTML = `
622
- <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>
623
- <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700">
624
- <i class="fas fa-trash-alt mr-1"></i> Remove
625
- </button>
626
- `;
627
-
628
- container.appendChild(newInputGroup);
629
-
630
- newInputGroup.querySelector('.remove-input').addEventListener('click', function() {
631
- newInputGroup.remove();
632
- updateRemoveButtons();
633
- });
634
-
635
  updateRemoveButtons();
636
  });
637
 
638
- function updateRemoveButtons() {
639
- const inputGroups = document.querySelectorAll('.text-input-group');
640
- inputGroups.forEach((group, index) => {
641
- const removeBtn = group.querySelector('.remove-input');
642
- if (inputGroups.length > 1) {
643
- removeBtn.classList.remove('hidden');
644
- } else {
645
- removeBtn.classList.add('hidden');
646
- }
647
- });
648
- }
649
-
650
- document.addEventListener('click', function(e) {
651
  if (e.target.closest('.remove-input')) {
652
  e.target.closest('.text-input-group').remove();
653
  updateRemoveButtons();
654
  }
655
  });
656
 
 
 
 
 
 
 
 
657
  document.getElementById('clearBtn').addEventListener('click', () => {
658
  document.getElementById('apiTestForm').reset();
659
  const container = document.getElementById('textInputsContainer');
660
- container.innerHTML = `
661
- <div class="text-input-group mb-4">
662
- <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>
663
- <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700 hidden">
664
- <i class="fas fa-trash-alt mr-1"></i> Remove
665
- </button>
666
- </div>
667
- `;
668
  document.getElementById('resultsSection').classList.add('hidden');
669
  });
670
 
@@ -672,25 +558,11 @@ def create_directories_and_files():
672
  e.preventDefault();
673
 
674
  const apiKey = document.getElementById('apiKey').value;
675
- const model = document.getElementById('model').value;
676
- const textInputs = document.querySelectorAll('#textInputsContainer textarea');
677
 
678
- if (!apiKey) {
679
- alert('Please enter your API key');
680
- return;
681
- }
682
-
683
- const texts = [];
684
- textInputs.forEach(input => {
685
- if (input.value.trim()) {
686
- texts.push(input.value.trim());
687
- }
688
- });
689
-
690
- if (texts.length === 0) {
691
- alert('Please enter at least one text to analyze');
692
- return;
693
- }
694
 
695
  const analyzeBtn = document.getElementById('analyzeBtn');
696
  const originalBtnContent = analyzeBtn.innerHTML;
@@ -702,29 +574,19 @@ def create_directories_and_files():
702
  try {
703
  const response = await fetch('/v1/moderations', {
704
  method: 'POST',
705
- headers: {
706
- 'Content-Type': 'application/json',
707
- 'Authorization': 'Bearer ' + apiKey
708
- },
709
- body: JSON.stringify({
710
- model: model,
711
- input: texts
712
- })
713
  });
714
 
715
- const endTime = Date.now();
716
- const responseTime = endTime - startTime;
717
-
718
- if (!response.ok) {
719
- const errorData = await response.json();
720
- throw new Error(errorData.error || 'Failed to analyze text');
721
- }
722
 
723
  const data = await response.json();
 
 
724
  displayResults(data, responseTime, texts);
 
725
 
726
  } catch (error) {
727
- console.error('Error analyzing text:', error);
728
  alert('Error: ' + error.message);
729
  } finally {
730
  analyzeBtn.innerHTML = originalBtnContent;
@@ -737,8 +599,6 @@ def create_directories_and_files():
737
  const resultsContainer = document.getElementById('resultsContainer');
738
 
739
  document.getElementById('responseTime').textContent = responseTime + 'ms';
740
- document.getElementById('tokenCount').textContent = data.usage.total_tokens.toLocaleString();
741
-
742
  resultsContainer.innerHTML = '';
743
 
744
  data.results.forEach((result, index) => {
@@ -749,54 +609,45 @@ def create_directories_and_files():
749
  ? '<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>'
750
  : '<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>';
751
 
752
- let categoriesHtml = '';
753
- for (const [category, isFlagged] of Object.entries(result.categories)) {
754
- const score = result.category_scores[category];
755
  const categoryClass = isFlagged ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400';
756
  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';
757
-
758
- categoriesHtml += `
759
- <div class="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700">
760
- <span class="font-medium capitalize">${category.replace('_', ' ')}</span>
761
  <div class="flex items-center">
762
- <span class="${categoryClass} mr-2">${isFlagged ? 'Flagged' : 'Safe'}</span>
763
  <span class="text-sm ${scoreClass} font-mono">${score.toFixed(4)}</span>
764
  </div>
765
  </div>
766
  `;
767
- }
768
 
769
  resultCard.innerHTML = `
770
  <div class="flex justify-between items-start mb-3">
771
- <h4 class="text-lg font-semibold">Text ${index + 1}</h4>
772
  ${flaggedBadge}
773
  </div>
774
- <div class="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg text-sm">
775
- ${texts[index]}
776
- </div>
777
  <div class="category-card">
778
- <h5 class="font-medium mb-2">Categories</h5>
779
- <div class="bg-white dark:bg-gray-700 rounded-lg overflow-hidden">
780
  ${categoriesHtml}
781
  </div>
782
  </div>
783
  `;
784
-
785
  resultsContainer.appendChild(resultCard);
786
  });
787
 
788
  resultsSection.classList.remove('hidden');
789
- resultsSection.scrollIntoView({ behavior: 'smooth' });
790
  }
791
 
792
  document.addEventListener('DOMContentLoaded', () => {
793
  initActivityChart();
794
-
795
  document.getElementById('refreshMetrics').addEventListener('click', fetchMetrics);
796
-
797
  fetchMetrics();
798
-
799
- setInterval(fetchMetrics, 30000);
800
  });
801
  </script>
802
  </body>
@@ -804,6 +655,8 @@ def create_directories_and_files():
804
 
805
  if __name__ == '__main__':
806
  create_directories_and_files()
807
-
808
  port = int(os.getenv('PORT', 7860))
809
- app.run(host='0.0.0.0', port=port, debug=True)
 
 
 
 
1
+ from flask import Flask, request, jsonify, render_template
2
  import os
3
  import uuid
4
  import time
 
10
 
11
  app = Flask(__name__, static_folder='static', template_folder='templates')
12
 
13
+ print("Loading Detoxify model... This may take a moment.")
14
  detoxify_model = Detoxify('multilingual')
15
+ print("Model loaded successfully.")
16
 
17
  API_KEY = os.getenv('API_KEY', 'your-api-key-here')
18
 
19
+ # --- Geliştirilmiş Metrik Takip Sistemi ---
20
+ # Son 100 isteğin süresini tutarak daha dinamik bir ortalama elde ederiz
21
+ request_durations = deque(maxlen=100)
22
+ # Son 10 dakika içindeki isteklerin zaman damgalarını tutarak RPM hesaplarız
23
+ request_timestamps = deque(maxlen=1000)
24
+
25
  daily_requests = defaultdict(int)
26
  daily_tokens = defaultdict(int)
27
  concurrent_requests = 0
 
40
 
41
  scores = {}
42
  for key in category_keys:
43
+ # Değerleri float'a çevirerek JSON uyumluluğunu garantiliyoruz
44
+ scores[key] = float(prediction_dict.get(key, 0.0))
45
 
46
  threshold = 0.5
47
  bool_categories = {key: (scores[key] > threshold) for key in category_keys}
 
52
 
53
  def track_request_metrics(start_time, tokens_count):
54
  end_time = time.time()
55
+ duration = end_time - start_time
56
+
57
+ request_durations.append(duration)
58
+ request_timestamps.append(datetime.now())
59
 
60
  today = datetime.now().strftime("%Y-%m-%d")
61
  daily_requests[today] += 1
 
66
  with concurrent_requests_lock:
67
  current_concurrent = concurrent_requests
68
 
69
+ # Ortalama ve Zirve Yanıt Süresi (Son 100 istek üzerinden)
70
+ if not request_durations:
71
+ avg_request_time = 0
72
+ peak_request_time = 0
73
+ else:
74
+ avg_request_time = sum(request_durations) / len(request_durations)
75
+ peak_request_time = max(request_durations)
76
+
77
+ # RPM (Requests Per Minute) - Dakikadaki İstek Sayısı
78
+ now = datetime.now()
79
+ one_minute_ago = now - timedelta(seconds=60)
80
+ requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
81
 
82
+ today = now.strftime("%Y-%m-%d")
83
  today_requests = daily_requests.get(today, 0)
84
  today_tokens = daily_tokens.get(today, 0)
85
 
86
  last_7_days = []
87
  for i in range(7):
88
+ date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
89
  last_7_days.append({
90
  "date": date,
91
  "requests": daily_requests.get(date, 0),
 
93
  })
94
 
95
  return {
96
+ "avg_request_time_ms": avg_request_time * 1000,
97
+ "peak_request_time_ms": peak_request_time * 1000,
98
+ "requests_per_minute": requests_last_minute,
99
  "concurrent_requests": current_concurrent,
100
  "today_requests": today_requests,
101
  "today_tokens": today_tokens,
 
126
  return jsonify({"error": "Unauthorized"}), 401
127
 
128
  data = request.get_json()
129
+ raw_input = data.get('input')
130
 
131
  if raw_input is None:
132
+ return jsonify({"error": "Invalid input, 'input' field is required"}), 400
133
 
134
  if isinstance(raw_input, str):
135
  texts = [raw_input]
 
138
  else:
139
  return jsonify({"error": "Invalid input format, expected string or list of strings"}), 400
140
 
141
+ if not texts:
142
+ return jsonify({"error": "Input list cannot be empty"}), 400
143
+
144
  if len(texts) > 10:
145
  return jsonify({"error": "Too many input items. Maximum 10 allowed."}), 400
146
 
147
  for text in texts:
148
+ if not isinstance(text, str) or len(text.encode('utf-8')) > 300000:
149
+ return jsonify({"error": "Each input item must be a string with a maximum of 300k bytes."}), 400
150
  total_tokens += count_tokens(text)
151
 
152
  results = []
153
+ # Detoxify'a tüm listeyi tek seferde vermek performansı artırır
154
+ predictions = detoxify_model.predict(texts)
155
+ # Predictions'ı her metin için ayrı bir sözlüğe dönüştür
156
+ for i in range(len(texts)):
157
+ single_prediction = {key: value[i] for key, value in predictions.items()}
158
+ flagged, bool_categories, scores, cat_applied_input_types = transform_predictions(single_prediction)
159
 
160
  results.append({
161
  "flagged": flagged,
162
  "categories": bool_categories,
163
  "category_scores": scores,
 
164
  })
165
 
166
  track_request_metrics(start_time, total_tokens)
167
 
168
  response_data = {
169
  "id": "modr-" + uuid.uuid4().hex[:24],
170
+ "model": "text-moderation-detoxify-multilingual",
171
+ "results": results
 
 
 
 
172
  }
173
 
174
  return jsonify(response_data)
175
 
176
+ except Exception as e:
177
+ print(f"An error occurred: {e}")
178
+ return jsonify({"error": "An internal server error occurred."}), 500
179
  finally:
180
  with concurrent_requests_lock:
181
  concurrent_requests -= 1
 
198
 
199
  index_path = os.path.join('templates', 'index.html')
200
  if not os.path.exists(index_path):
201
+ with open(index_path, 'w', encoding='utf-8') as f:
202
  f.write('''<!DOCTYPE html>
203
  <html lang="en">
204
  <head>
 
215
  extend: {
216
  colors: {
217
  primary: {
218
+ 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
219
+ 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a',
 
 
 
 
 
 
 
 
220
  }
221
  }
222
  }
 
224
  }
225
  </script>
226
  <style>
227
+ .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
228
+ .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); }
 
 
 
 
229
  .glass-effect {
230
+ background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
 
231
  border: 1px solid rgba(255, 255, 255, 0.2);
232
  }
233
+ .dark .glass-effect { background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(100, 116, 139, 0.3); }
234
+ .category-card { transition: all 0.3s ease; }
235
+ .category-card:hover { transform: translateY(-5px); }
236
+ .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; }
237
+ @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  </style>
239
  </head>
240
+ <body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen font-sans">
241
  <header class="gradient-bg text-white shadow-lg">
242
  <div class="container mx-auto px-4 py-6 flex justify-between items-center">
243
  <div class="flex items-center space-x-3">
 
266
  </h2>
267
 
268
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
269
+ <!-- METRIC CARD 1: AVG RESPONSE TIME -->
270
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
271
  <div class="flex items-center justify-between">
272
  <div>
273
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response (last 100)</p>
274
  <p class="text-2xl font-bold" id="avgResponseTime">0ms</p>
275
  </div>
276
  <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
 
279
  </div>
280
  </div>
281
 
282
+ <!-- METRIC CARD 2: REQUESTS PER MINUTE -->
283
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
284
  <div class="flex items-center justify-between">
285
  <div>
286
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Requests / Minute</p>
287
+ <p class="text-2xl font-bold" id="requestsPerMinute">0</p>
288
  </div>
289
+ <div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
290
+ <i class="fas fa-tachometer-alt text-green-600 dark:text-green-400"></i>
291
  </div>
292
  </div>
293
  </div>
294
 
295
+ <!-- METRIC CARD 3: PEAK RESPONSE TIME -->
296
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
297
  <div class="flex items-center justify-between">
298
  <div>
299
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Peak Response (last 100)</p>
300
+ <p class="text-2xl font-bold" id="peakResponseTime">0ms</p>
301
  </div>
302
+ <div class="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
303
+ <i class="fas fa-exclamation-triangle text-red-600 dark:text-red-400"></i>
304
  </div>
305
  </div>
306
  </div>
307
 
308
+ <!-- METRIC CARD 4: TODAY'S REQUESTS -->
309
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
310
  <div class="flex items-center justify-between">
311
  <div>
312
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Today's Requests</p>
313
+ <p class="text-2xl font-bold" id="todayRequests">0</p>
314
  </div>
315
+ <div class="w-12 h-12 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
316
+ <i class="fas fa-list-ol text-yellow-600 dark:text-yellow-400"></i>
317
  </div>
318
  </div>
319
  </div>
 
327
  </div>
328
  </section>
329
 
330
+ <!-- API Tester Section -->
331
  <section class="mb-12">
332
  <h2 class="text-2xl font-bold mb-6 flex items-center">
333
  <i class="fas fa-code mr-3 text-primary-600"></i>
 
336
 
337
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
338
  <form id="apiTestForm">
339
+ <div class="mb-6">
340
+ <label class="block text-sm font-medium mb-2" for="apiKey">API Key</label>
341
+ <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">
 
 
 
 
 
 
 
 
 
342
  </div>
343
 
344
  <div class="mb-6">
 
373
  </div>
374
  </section>
375
 
376
+ <!-- Results Section -->
377
  <section id="resultsSection" class="hidden">
378
  <h2 class="text-2xl font-bold mb-6 flex items-center">
379
  <i class="fas fa-clipboard-check mr-3 text-primary-600"></i>
 
384
  <div class="flex justify-between items-center mb-4">
385
  <h3 class="text-lg font-semibold">Summary</h3>
386
  <div class="text-sm text-gray-500 dark:text-gray-400">
387
+ <i class="fas fa-clock mr-1"></i> Response time: <span id="responseTime">0ms</span>
 
388
  </div>
389
  </div>
390
 
 
393
  </div>
394
  </section>
395
 
396
+ <!-- API Documentation -->
397
  <section>
398
  <h2 class="text-2xl font-bold mb-6 flex items-center">
399
  <i class="fas fa-book mr-3 text-primary-600"></i>
400
  API Documentation
401
  </h2>
 
402
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
403
  <h3 class="text-lg font-semibold mb-4">Endpoint</h3>
404
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">POST /v1/moderations</div>
 
 
 
405
  <h3 class="text-lg font-semibold mb-4">Request Body</h3>
406
  <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 overflow-x-auto">
407
  <pre class="text-sm"><code>{
 
408
  "input": "Text to moderate"
409
  }</code></pre>
410
  </div>
 
411
  <h3 class="text-lg font-semibold mb-4">Response</h3>
412
  <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
413
  <pre class="text-sm"><code>{
414
  "id": "modr-1234567890abcdef",
415
+ "model": "text-moderation-detoxify-multilingual",
416
  "results": [
417
  {
418
  "flagged": true,
419
  "categories": {
420
  "toxicity": true,
421
  "severe_toxicity": false,
422
+ /* ... other categories */
 
 
 
 
423
  },
424
  "category_scores": {
425
  "toxicity": 0.95,
426
  "severe_toxicity": 0.1,
427
+ /* ... other scores */
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  }
429
  }
430
+ ]
 
 
 
 
431
  }</code></pre>
432
  </div>
433
  </div>
 
435
  </main>
436
 
437
  <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
438
+ <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400">
439
+ © 2025 Text Moderation API. All rights reserved.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  </div>
441
  </footer>
442
 
 
444
  const darkModeToggle = document.getElementById('darkModeToggle');
445
  const html = document.documentElement;
446
 
447
+ if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
 
448
  html.classList.add('dark');
449
  }
450
 
451
  darkModeToggle.addEventListener('click', () => {
452
  html.classList.toggle('dark');
453
+ localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
 
454
  });
455
 
456
  let activityChart;
457
 
458
  function initActivityChart() {
459
+ if (activityChart) { activityChart.destroy(); }
460
  const ctx = document.getElementById('activityChart').getContext('2d');
461
+ const isDarkMode = document.documentElement.classList.contains('dark');
462
+ const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
463
+ const textColor = isDarkMode ? '#e5e7eb' : '#374151';
464
+
465
  activityChart = new Chart(ctx, {
466
  type: 'bar',
467
+ data: { labels: [], datasets: [
468
+ { label: 'Requests', data: [], backgroundColor: 'rgba(59, 130, 246, 0.6)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1 },
469
+ { label: 'Tokens', data: [], backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgba(16, 185, 129, 1)', borderWidth: 1, yAxisID: 'y1' }
470
+ ]},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  options: {
472
+ responsive: true, maintainAspectRatio: false,
 
473
  scales: {
474
+ y: { beginAtZero: true, position: 'left', title: { display: true, text: 'Requests', color: textColor }, ticks: { color: textColor }, grid: { color: gridColor } },
475
+ y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Tokens', color: textColor }, ticks: { color: textColor }, grid: { drawOnChartArea: false } }
476
+ },
477
+ plugins: { legend: { labels: { color: textColor } } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
  });
480
  }
481
 
482
  async function fetchMetrics() {
483
+ const apiKey = document.getElementById('apiKey').value || 'temp-key-for-metrics';
 
 
 
 
 
484
  try {
485
  const response = await fetch('/v1/metrics', {
486
+ headers: { 'Authorization': 'Bearer ' + apiKey }
 
 
 
 
487
  });
 
488
  if (!response.ok) {
489
+ const error = await response.json();
490
+ console.error('Failed to fetch metrics:', error.error);
491
+ if(response.status === 401) {
492
+ // Maybe show a small warning that API key is needed for metrics
493
+ }
494
+ return;
495
  }
 
496
  const data = await response.json();
497
  updateMetricsDisplay(data);
498
  } catch (error) {
 
501
  }
502
 
503
  function updateMetricsDisplay(data) {
504
+ document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
505
+ document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms';
506
+ document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
507
+ document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString();
508
 
509
  if (activityChart) {
510
+ const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse();
511
+ const requests = data.last_7_days.map(d => d.requests).reverse();
512
+ const tokens = data.last_7_days.map(d => d.tokens).reverse();
 
 
 
 
513
 
514
  activityChart.data.labels = labels;
515
  activityChart.data.datasets[0].data = requests;
 
517
  activityChart.update();
518
  }
519
  }
520
+
521
  document.getElementById('addTextInput').addEventListener('click', () => {
522
  const container = document.getElementById('textInputsContainer');
523
+ if (container.children.length >= 10) {
 
 
524
  alert('Maximum 10 text inputs allowed');
525
  return;
526
  }
527
+ const newGroup = container.firstElementChild.cloneNode(true);
528
+ newGroup.querySelector('textarea').value = '';
529
+ newGroup.querySelector('.remove-input').classList.remove('hidden');
530
+ container.appendChild(newGroup);
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  updateRemoveButtons();
532
  });
533
 
534
+ document.getElementById('textInputsContainer').addEventListener('click', function(e) {
 
 
 
 
 
 
 
 
 
 
 
 
535
  if (e.target.closest('.remove-input')) {
536
  e.target.closest('.text-input-group').remove();
537
  updateRemoveButtons();
538
  }
539
  });
540
 
541
+ function updateRemoveButtons() {
542
+ const groups = document.querySelectorAll('.text-input-group');
543
+ groups.forEach(group => {
544
+ group.querySelector('.remove-input').classList.toggle('hidden', groups.length <= 1);
545
+ });
546
+ }
547
+
548
  document.getElementById('clearBtn').addEventListener('click', () => {
549
  document.getElementById('apiTestForm').reset();
550
  const container = document.getElementById('textInputsContainer');
551
+ container.innerHTML = container.firstElementChild.outerHTML;
552
+ container.querySelector('textarea').value = '';
553
+ updateRemoveButtons();
 
 
 
 
 
554
  document.getElementById('resultsSection').classList.add('hidden');
555
  });
556
 
 
558
  e.preventDefault();
559
 
560
  const apiKey = document.getElementById('apiKey').value;
561
+ if (!apiKey) { alert('Please enter your API key'); return; }
 
562
 
563
+ const texts = Array.from(document.querySelectorAll('#textInputsContainer textarea'))
564
+ .map(t => t.value.trim()).filter(Boolean);
565
+ if (texts.length === 0) { alert('Please enter at least one text to analyze'); return; }
 
 
 
 
 
 
 
 
 
 
 
 
 
566
 
567
  const analyzeBtn = document.getElementById('analyzeBtn');
568
  const originalBtnContent = analyzeBtn.innerHTML;
 
574
  try {
575
  const response = await fetch('/v1/moderations', {
576
  method: 'POST',
577
+ headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
578
+ body: JSON.stringify({ input: texts.length === 1 ? texts[0] : texts })
 
 
 
 
 
 
579
  });
580
 
581
+ const responseTime = Date.now() - startTime;
 
 
 
 
 
 
582
 
583
  const data = await response.json();
584
+ if (!response.ok) throw new Error(data.error || 'Failed to analyze text');
585
+
586
  displayResults(data, responseTime, texts);
587
+ fetchMetrics(); // Update metrics immediately after a successful request
588
 
589
  } catch (error) {
 
590
  alert('Error: ' + error.message);
591
  } finally {
592
  analyzeBtn.innerHTML = originalBtnContent;
 
599
  const resultsContainer = document.getElementById('resultsContainer');
600
 
601
  document.getElementById('responseTime').textContent = responseTime + 'ms';
 
 
602
  resultsContainer.innerHTML = '';
603
 
604
  data.results.forEach((result, index) => {
 
609
  ? '<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>'
610
  : '<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>';
611
 
612
+ let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => {
613
+ const isFlagged = result.categories[category];
 
614
  const categoryClass = isFlagged ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400';
615
  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';
616
+ return `
617
+ <div class="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
618
+ <span class="font-medium capitalize">${category.replace(/_/g, ' ')}</span>
 
619
  <div class="flex items-center">
 
620
  <span class="text-sm ${scoreClass} font-mono">${score.toFixed(4)}</span>
621
  </div>
622
  </div>
623
  `;
624
+ }).join('');
625
 
626
  resultCard.innerHTML = `
627
  <div class="flex justify-between items-start mb-3">
628
+ <h4 class="text-lg font-semibold">Input ${index + 1}</h4>
629
  ${flaggedBadge}
630
  </div>
631
+ <blockquote class="mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg text-sm border-l-4 border-gray-300 dark:border-gray-500">${texts[index]}</blockquote>
 
 
632
  <div class="category-card">
633
+ <h5 class="font-medium mb-2">Category Scores</h5>
634
+ <div class="bg-white dark:bg-gray-800/50 rounded-lg p-2">
635
  ${categoriesHtml}
636
  </div>
637
  </div>
638
  `;
 
639
  resultsContainer.appendChild(resultCard);
640
  });
641
 
642
  resultsSection.classList.remove('hidden');
643
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
644
  }
645
 
646
  document.addEventListener('DOMContentLoaded', () => {
647
  initActivityChart();
 
648
  document.getElementById('refreshMetrics').addEventListener('click', fetchMetrics);
 
649
  fetchMetrics();
650
+ setInterval(fetchMetrics, 15000); // Refresh metrics every 15 seconds
 
651
  });
652
  </script>
653
  </body>
 
655
 
656
  if __name__ == '__main__':
657
  create_directories_and_files()
 
658
  port = int(os.getenv('PORT', 7860))
659
+ # debug=True'yu production ortamında False yapın.
660
+ # Modelin yüklenmesi uzun sürdüğü için `use_reloader=False` eklemek,
661
+ # geliştirme sırasında her dosya değişikliğinde modeli tekrar yüklemesini engeller.
662
+ app.run(host='0.0.0.0', port=port, debug=True, use_reloader=False)