nixaut-codelabs commited on
Commit
509d66d
·
verified ·
1 Parent(s): fadc7dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +995 -269
app.py CHANGED
@@ -1,153 +1,121 @@
 
1
  import os
2
  import uuid
3
  import time
4
  import threading
5
- import io
 
 
6
  from datetime import datetime, timedelta
7
  from collections import defaultdict, deque
8
-
9
- from flask import Flask, request, jsonify, render_template
10
  from detoxify import Detoxify
11
- import numpy as np
12
- import requests
13
- from PIL import Image
14
- from tensorflow.keras.models import load_model
15
 
16
- app = Flask(__name__, static_folder='static', template_folder='templates')
17
- app.logger.setLevel('INFO')
18
 
19
- API_KEY = os.environ.get('API_KEY')
20
- if not API_KEY:
21
- raise ValueError("API_KEY environment variable not set.")
22
 
23
- print("Loading Detoxify model for text moderation...")
24
  detoxify_model = Detoxify('multilingual')
25
  print("Detoxify model loaded successfully.")
26
 
27
- MODEL_PATH = 'keras_model.h5'
28
- LABELS_PATH = 'labels.txt'
29
- image_model = None
30
- image_labels = None
31
-
32
- try:
33
- print("Loading Teachable Machine model for image moderation...")
34
- image_model = load_model(MODEL_PATH, compile=False)
35
- with open(LABELS_PATH, 'r') as f:
36
- image_labels = [line.strip().split(' ')[1] for line in f.readlines()]
37
- print("Image moderation model loaded successfully.")
38
- except Exception as e:
39
- app.logger.warning(f"Could not load image moderation model. Image moderation will be disabled. Error: {e}")
40
- image_model = None
41
- image_labels = None
42
-
43
- request_durations = deque(maxlen=100)
44
- request_timestamps = deque(maxlen=1000)
45
  daily_requests = defaultdict(int)
 
46
  concurrent_requests = 0
47
  concurrent_requests_lock = threading.Lock()
48
 
49
- def is_url(string):
50
- return isinstance(string, str) and string.strip().startswith(('http://', 'https://'))
51
 
52
- def classify_image(image_bytes):
53
- if not image_model or not image_labels:
54
- raise RuntimeError("Image moderation model is not available.")
55
-
56
- image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
57
- image = image.resize((224, 224))
58
-
59
- image_array = np.asarray(image)
60
- normalized_image_array = (image_array.astype(np.float32) / 127.5) - 1
61
-
62
- data = np.ndarray(shape=(1, 224, 224, 3), dtype=np.float32)
63
- data[0] = normalized_image_array
64
-
65
- prediction = image_model.predict(data)
66
-
67
- scores = {label.lower(): float(score) for label, score in zip(image_labels, prediction[0])}
68
- return scores
 
 
69
 
70
- def transform_text_predictions(prediction_dict):
71
  category_keys = [
72
- "toxicity", "severe_toxicity", "obscene", "threat",
73
  "insult", "identity_attack", "sexual_explicit"
74
  ]
75
- scores = {key: float(prediction_dict.get(key, 0.0)) for key in category_keys}
 
 
 
76
 
77
  threshold = 0.5
78
- categories = {key: (scores[key] > threshold) for key in category_keys}
79
- flagged = any(categories.values())
80
 
81
- return flagged, categories, scores
82
 
83
- def transform_image_predictions(prediction_dict):
84
- nsfw_score = prediction_dict.get('nsfw', 0.0)
85
-
86
- categories = {
87
- "sexual": nsfw_score > 0.8,
88
- "hate": False,
89
- "harassment": False,
90
- "self-harm": False,
91
- "sexual/minors": nsfw_score > 0.9,
92
- "hate/threatening": False,
93
- "violence/graphic": False,
94
- "self-harm/intent": False,
95
- "self-harm/instructions": False,
96
- "harassment/threatening": False,
97
- "violence": False,
98
- }
99
-
100
- category_scores = {
101
- "sexual": nsfw_score,
102
- "hate": 0.0,
103
- "harassment": 0.0,
104
- "self-harm": 0.0,
105
- "sexual/minors": nsfw_score,
106
- "hate/threatening": 0.0,
107
- "violence/graphic": 0.0,
108
- "self-harm/intent": 0.0,
109
- "self-harm/instructions": 0.0,
110
- "harassment/threatening": 0.0,
111
- "violence": 0.0,
112
- }
113
 
114
- flagged = any(categories.values())
115
 
116
- return flagged, categories, category_scores
117
-
118
- def track_request_metrics(start_time):
119
- duration = time.time() - start_time
120
  request_durations.append(duration)
121
  request_timestamps.append(datetime.now())
 
122
  today = datetime.now().strftime("%Y-%m-%d")
123
  daily_requests[today] += 1
 
124
 
125
  def get_performance_metrics():
 
126
  with concurrent_requests_lock:
127
  current_concurrent = concurrent_requests
128
 
129
- avg_request_time = sum(request_durations) / len(request_durations) if request_durations else 0
130
- peak_request_time = max(request_durations) if request_durations else 0
131
-
 
 
 
 
132
  now = datetime.now()
133
  one_minute_ago = now - timedelta(seconds=60)
134
  requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
135
- today_requests = daily_requests.get(now.strftime("%Y-%m-%d"), 0)
136
-
 
 
 
137
  last_7_days = []
138
  for i in range(7):
139
  date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
140
  last_7_days.append({
141
  "date": date,
142
  "requests": daily_requests.get(date, 0),
 
143
  })
144
-
145
  return {
146
  "avg_request_time_ms": avg_request_time * 1000,
147
  "peak_request_time_ms": peak_request_time * 1000,
148
  "requests_per_minute": requests_last_minute,
149
  "concurrent_requests": current_concurrent,
150
  "today_requests": today_requests,
 
151
  "last_7_days": last_7_days
152
  }
153
 
@@ -159,104 +127,654 @@ def home():
159
  def moderations():
160
  global concurrent_requests
161
 
162
- auth_header = request.headers.get('Authorization')
163
- if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY:
164
- return jsonify({"error": {"message": "Incorrect API key provided.", "type": "invalid_request_error", "code": "invalid_api_key"}}), 401
165
-
166
  with concurrent_requests_lock:
167
  concurrent_requests += 1
168
 
169
  start_time = time.time()
 
170
 
 
171
  try:
 
 
 
 
 
 
 
 
 
 
172
  data = request.get_json()
173
- if not data:
174
- return jsonify({"error": "Invalid JSON body"}), 400
175
-
176
  raw_input = data.get('input')
 
177
  if raw_input is None:
178
- return jsonify({"error": "'input' field is required"}), 400
179
-
180
- inputs = [raw_input] if isinstance(raw_input, str) else raw_input
181
- if not isinstance(inputs, list):
182
- return jsonify({"error": "'input' must be a string or a list of strings/URLs"}), 400
 
 
 
 
 
 
 
 
 
183
 
 
 
 
 
184
  results = []
185
- texts_to_process = []
186
- text_indices = []
187
-
188
- for i, item in enumerate(inputs):
189
- if is_url(item):
190
- try:
191
- response = requests.get(item, timeout=10)
192
- response.raise_for_status()
193
- image_scores = classify_image(response.content)
194
- flagged, categories, category_scores = transform_image_predictions(image_scores)
195
- results.append((i, {"flagged": flagged, "categories": categories, "category_scores": category_scores}))
196
- except requests.RequestException as e:
197
- results.append((i, {"error": f"Failed to download image: {e}"}))
198
- except Exception as e:
199
- results.append((i, {"error": f"Failed to process image: {e}"}))
200
- elif isinstance(item, str):
201
- texts_to_process.append(item)
202
- text_indices.append(i)
203
- else:
204
- results.append((i, {"error": "Invalid input type. Must be a string or URL."}))
205
-
206
- if texts_to_process:
207
- text_predictions = detoxify_model.predict(texts_to_process)
208
- for i, original_index in enumerate(text_indices):
209
- single_prediction = {key: value[i] for key, value in text_predictions.items()}
210
- flagged, categories, category_scores = transform_text_predictions(single_prediction)
211
- results.append((original_index, {"flagged": flagged, "categories": categories, "category_scores": category_scores}))
 
 
 
 
 
 
 
 
 
 
 
212
 
213
- results.sort(key=lambda x: x[0])
214
- final_results = [res for _, res in results]
215
-
216
  response_data = {
217
  "id": "modr-" + uuid.uuid4().hex[:24],
218
- "model": "smart-moderator-multimodal-v1",
219
- "results": final_results
220
  }
221
 
222
- return jsonify(response_data)
223
-
 
224
  except Exception as e:
225
  app.logger.error(f"An error occurred: {e}", exc_info=True)
226
- return jsonify({"error": "An internal server error occurred."}), 500
 
227
  finally:
228
- track_request_metrics(start_time)
 
 
229
  with concurrent_requests_lock:
230
  concurrent_requests -= 1
231
 
232
  @app.route('/v1/metrics', methods=['GET'])
233
  def metrics():
234
  auth_header = request.headers.get('Authorization')
235
- if not auth_header or not auth_header.startswith("Bearer ") or auth_header.split(" ")[1] != API_KEY:
 
 
 
 
236
  return jsonify({"error": "Unauthorized"}), 401
237
 
238
  return jsonify(get_performance_metrics())
239
 
240
- def create_app_structure():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  os.makedirs('templates', exist_ok=True)
242
  os.makedirs('static', exist_ok=True)
243
 
244
- index_html_content = r'''
245
- <!DOCTYPE html>
 
 
246
  <html lang="en">
247
  <head>
248
  <meta charset="UTF-8">
249
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
250
- <title>Smart Moderator API</title>
251
  <script src="https://cdn.tailwindcss.com"></script>
252
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
253
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
254
  <script>
255
- tailwind.config = { darkMode: 'class' }
 
 
 
 
 
 
 
 
 
 
 
 
256
  </script>
257
  <style>
258
  .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
259
  .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); }
 
 
 
 
 
 
 
260
  .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; }
261
  @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
262
  </style>
@@ -266,12 +784,15 @@ def create_app_structure():
266
  <div class="container mx-auto px-4 py-6 flex justify-between items-center">
267
  <div class="flex items-center space-x-3">
268
  <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center">
269
- <i class="fas fa-shield-alt text-indigo-600 text-xl"></i>
270
  </div>
271
  <h1 class="text-2xl font-bold">Smart Moderator</h1>
272
  </div>
273
- <div>
274
- <button id="darkModeToggle" class="bg-white/20 p-2 rounded-lg hover:bg-white/30 transition">
 
 
 
275
  <i class="fas fa-moon dark:hidden"></i>
276
  <i class="fas fa-sun hidden dark:inline"></i>
277
  </button>
@@ -281,190 +802,395 @@ def create_app_structure():
281
 
282
  <main class="container mx-auto px-4 py-8">
283
  <section class="mb-12">
284
- <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-tachometer-alt mr-3 text-indigo-500"></i>Performance Metrics</h2>
 
 
 
 
285
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
286
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response</p><p class="text-2xl font-bold" id="avgResponseTime">0ms</p></div>
287
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Requests / Minute</p><p class="text-2xl font-bold" id="requestsPerMinute">0</p></div>
288
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Peak Response</p><p class="text-2xl font-bold" id="peakResponseTime">0ms</p></div>
289
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"><p class="text-gray-500 dark:text-gray-400 text-sm">Today's Requests</p><p class="text-2xl font-bold" id="todayRequests">0</p></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  </div>
 
291
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
292
  <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3>
293
- <div class="h-64"><canvas id="activityChart"></canvas></div>
 
 
294
  </div>
295
  </section>
296
 
297
  <section class="mb-12">
298
- <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-vial mr-3 text-indigo-500"></i>API Tester</h2>
 
 
 
 
299
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
300
  <form id="apiTestForm">
301
- <div class="mb-4"><label class="block text-sm font-medium mb-2" for="apiKey">API Key</label><input type="password" id="apiKey" class="w-full px-4 py-2 rounded-lg border bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your API key"></div>
302
- <div class="mb-4"><label class="block text-sm font-medium mb-2">Input (Text or Image URL)</label><textarea id="apiInput" class="w-full px-4 py-2 rounded-lg border bg-white dark:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500" rows="4" placeholder="Enter text to moderate, or a public image URL. For multiple items, separate them with a new line."></textarea></div>
303
- <button type="submit" id="analyzeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-6 rounded-lg transition"><i class="fas fa-search mr-2"></i>Analyze</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  </form>
305
  </div>
306
  </section>
307
 
308
  <section id="resultsSection" class="hidden">
309
- <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-clipboard-check mr-3 text-indigo-500"></i>Analysis Results</h2>
310
- <div id="resultsContainer" class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  </section>
312
-
313
  <section>
314
- <h2 class="text-2xl font-bold mb-6 flex items-center"><i class="fas fa-book-open mr-3 text-indigo-500"></i>API Documentation</h2>
 
 
 
315
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
316
- <h3 class="text-lg font-semibold mb-2">Endpoint</h3>
317
- <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">POST /v1/moderations</code>
318
- <h3 class="text-lg font-semibold mb-2">Headers</h3>
319
- <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">Authorization: Bearer YOUR_API_KEY<br>Content-Type: application/json</code>
320
- <h3 class="text-lg font-semibold mb-2">Request Body</h3>
321
- <p class="text-sm mb-2">The `input` field can be a single string/URL or a list of strings/URLs.</p>
322
- <code class="block bg-gray-100 dark:bg-gray-700 p-3 rounded-lg text-sm mb-4">{"input": "Text to moderate"}</code>
323
- <h3 class="text-lg font-semibold mb-2">Usage Example (cURL)</h3>
324
- <div class="space-y-4">
325
- <div>
326
- <h4 class="font-semibold text-md mb-1">Text Moderation</h4>
327
- <pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto text-sm"><code>curl -X POST https://nixaut-codelabs-smart-moderator.hf.space/v1/moderations \
328
- -H "Authorization: Bearer YOUR_API_KEY" \
329
- -H "Content-Type: application/json" \
330
- -d '{"input": "You are stupid and I hate you."}'</code></pre>
331
- </div>
332
- <div>
333
- <h4 class="font-semibold text-md mb-1">Multimodal Moderation (Text + Image)</h4>
334
- <pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto text-sm"><code>curl -X POST https://nixaut-codelabs-smart-moderator.hf.space/v1/moderations \
335
- -H "Authorization: Bearer YOUR_API_KEY" \
336
- -H "Content-Type: application/json" \
337
- -d '{"input": [
338
- "This is a perfectly normal sentence.",
339
- "https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg"
340
- ]}'</code></pre>
341
- </div>
 
 
 
 
 
342
  </div>
343
  </div>
344
  </section>
345
  </main>
 
346
  <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
347
- <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400 text-sm">
348
- © 2024 Smart Moderator by Nix-Aut Codelabs | <a href="https://nixaut-codelabs-smart-moderator.hf.space" class="hover:underline">nixaut-codelabs-smart-moderator.hf.space</a>
349
  </div>
350
  </footer>
 
351
  <script>
352
  const darkModeToggle = document.getElementById('darkModeToggle');
 
 
353
  if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
354
- document.documentElement.classList.add('dark');
355
  }
 
356
  darkModeToggle.addEventListener('click', () => {
357
- document.documentElement.classList.toggle('dark');
358
- localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light');
359
  });
360
 
361
  let activityChart;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  async function fetchMetrics() {
363
- const apiKey = document.getElementById('apiKey').value || 'temp-key';
364
  try {
365
- const response = await fetch('/v1/metrics', { headers: { 'Authorization': 'Bearer ' + apiKey } });
366
- if (!response.ok) return;
367
- const data = await response.json();
368
- document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
369
- document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms';
370
- document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
371
- document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString();
372
-
373
- if (activityChart) {
374
- const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse();
375
- const requests = data.last_7_days.map(d => d.requests).reverse();
376
- activityChart.data.labels = labels;
377
- activityChart.data.datasets[0].data = requests;
378
- activityChart.update();
379
  }
380
- } catch (e) { console.error("Could not fetch metrics", e); }
 
 
 
 
381
  }
382
 
383
- function initChart() {
384
- const ctx = document.getElementById('activityChart').getContext('2d');
385
- const isDark = document.documentElement.classList.contains('dark');
386
- activityChart = new Chart(ctx, {
387
- type: 'bar',
388
- data: { labels: [], datasets: [{ label: 'Requests', data: [], backgroundColor: 'rgba(99, 102, 241, 0.6)' }] },
389
- options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } }
390
- });
 
 
 
 
 
 
 
 
391
  }
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  document.getElementById('apiTestForm').addEventListener('submit', async (e) => {
394
  e.preventDefault();
 
395
  const apiKey = document.getElementById('apiKey').value;
396
- if (!apiKey) { alert('Please enter an API key.'); return; }
 
 
 
 
397
 
398
- const rawInput = document.getElementById('apiInput').value.trim();
399
- if (!rawInput) { alert('Please enter text or a URL to analyze.'); return; }
400
-
401
- const inputs = rawInput.split('\\n').map(item => item.trim()).filter(Boolean);
402
- const body = { input: inputs.length === 1 ? inputs[0] : inputs };
403
-
404
  const analyzeBtn = document.getElementById('analyzeBtn');
 
 
405
  analyzeBtn.disabled = true;
406
- analyzeBtn.innerHTML = '<div class="loading-spinner inline-block w-4 h-4 border-2 border-white rounded-full mr-2"></div>Analyzing...';
407
-
 
408
  try {
409
  const response = await fetch('/v1/moderations', {
410
  method: 'POST',
411
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
412
- body: JSON.stringify(body)
413
  });
 
 
 
414
  const data = await response.json();
415
- displayResults(data);
 
 
416
  fetchMetrics();
 
417
  } catch (error) {
418
- alert('An error occurred: ' + error.message);
419
  } finally {
 
420
  analyzeBtn.disabled = false;
421
- analyzeBtn.innerHTML = '<i class="fas fa-search mr-2"></i>Analyze';
422
  }
423
  });
424
 
425
- function displayResults(data) {
 
426
  const resultsContainer = document.getElementById('resultsContainer');
 
 
427
  resultsContainer.innerHTML = '';
428
 
429
- if (data.error) {
430
- resultsContainer.innerHTML = `<div class="p-4 text-red-700 bg-red-100 dark:bg-red-900 dark:text-red-200 rounded-lg"><p><strong>Error:</strong> ${data.error.message || JSON.stringify(data.error)}</p></div>`;
431
- } else if (data.results) {
432
- data.results.forEach((result, index) => {
433
- const resultCard = document.createElement('div');
434
- resultCard.className = 'border-t border-gray-200 dark:border-gray-700 pt-4 mt-4 first:mt-0 first:border-t-0 first:pt-0';
435
- const flaggedBadge = result.flagged
436
- ? '<span class="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800">Flagged</span>'
437
- : '<span class="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">Safe</span>';
438
-
439
- let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => {
440
- const isFlagged = result.categories[category];
441
- return `<div class="flex justify-between py-1"><span class="${isFlagged ? 'font-bold text-red-500' : ''}">${category.replace("/", " / ")}</span><span class="font-mono text-sm">${score.toFixed(4)}</span></div>`;
442
- }).join('');
443
-
444
- resultCard.innerHTML = `<div class="flex justify-between items-center mb-2"><h4 class="font-bold">Input ${index + 1}</h4>${flaggedBadge}</div><div>${categoriesHtml}</div>`;
445
- resultsContainer.appendChild(resultCard);
446
- });
447
- }
448
- document.getElementById('resultsSection').classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  }
450
 
451
  document.addEventListener('DOMContentLoaded', () => {
452
- initChart();
 
453
  fetchMetrics();
454
- setInterval(fetchMetrics, 20000);
455
  });
456
  </script>
457
  </body>
458
  </html>
459
- '''
460
- index_path = os.path.join('templates', 'index.html')
461
- if not os.path.exists(index_path):
462
- with open(index_path, 'w', encoding='utf-8') as f:
463
- f.write(index_html_content)
464
 
465
  if __name__ == '__main__':
466
- create_app_structure()
467
- port = int(os.environ.get('PORT', 7860))
468
- # For production, use a proper WSGI server like Gunicorn
469
- # gunicorn --bind 0.0.0.0:7860 app:app
470
- app.run(host='0.0.0.0', port=port, debug=False)
 
1
+ from flask import Flask, request, jsonify, render_template
2
  import os
3
  import uuid
4
  import time
5
  import threading
6
+ import tiktoken
7
+ import requests
8
+ import base64
9
  from datetime import datetime, timedelta
10
  from collections import defaultdict, deque
 
 
11
  from detoxify import Detoxify
12
+ import logging
 
 
 
13
 
14
+ logging.basicConfig(level=logging.INFO)
 
15
 
16
+ app = Flask(__name__, static_folder='static', template_folder='templates')
 
 
17
 
18
+ print("Loading Detoxify model... This may take a moment.")
19
  detoxify_model = Detoxify('multilingual')
20
  print("Detoxify model loaded successfully.")
21
 
22
+ API_KEY = os.getenv('API_KEY', 'your-api-key-here')
23
+
24
+ request_durations = deque(maxlen=100)
25
+ request_timestamps = deque(maxlen=1000)
26
+
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  daily_requests = defaultdict(int)
28
+ daily_tokens = defaultdict(int)
29
  concurrent_requests = 0
30
  concurrent_requests_lock = threading.Lock()
31
 
32
+ encoding = tiktoken.get_encoding("cl100k_base")
 
33
 
34
+ NSFW_MODEL_URL = "https://teachablemachine.withgoogle.com/models/gJOADmf_u/"
35
+
36
+ def count_tokens(text):
37
+ return len(encoding.encode(text))
38
+
39
+ def classify_image_nsfw(image_data):
40
+ try:
41
+ response = requests.post(
42
+ NSFW_MODEL_URL,
43
+ json={"instances": [{"image_bytes": {"b64": image_data}}]},
44
+ timeout=10
45
+ )
46
+ if response.status_code == 200:
47
+ predictions = response.json().get('predictions', [{}])[0]
48
+ nsfw_score = predictions.get('nsfw', 0.0)
49
+ return nsfw_score > 0.5
50
+ return False
51
+ except:
52
+ return False
53
 
54
+ def transform_predictions(prediction_dict):
55
  category_keys = [
56
+ "toxicity", "severe_toxicity", "obscene", "threat",
57
  "insult", "identity_attack", "sexual_explicit"
58
  ]
59
+
60
+ scores = {}
61
+ for key in category_keys:
62
+ scores[key] = float(prediction_dict.get(key, 0.0))
63
 
64
  threshold = 0.5
65
+ bool_categories = {key: (scores[key] > threshold) for key in category_keys}
66
+ flagged = any(bool_categories.values())
67
 
68
+ return flagged, bool_categories, scores
69
 
70
+ def track_request_metrics(start_time, tokens_count):
71
+ end_time = time.time()
72
+ duration = end_time - start_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ app.logger.info(f"Server-side processing for moderation request took {duration * 1000:.2f} ms.")
75
 
 
 
 
 
76
  request_durations.append(duration)
77
  request_timestamps.append(datetime.now())
78
+
79
  today = datetime.now().strftime("%Y-%m-%d")
80
  daily_requests[today] += 1
81
+ daily_tokens[today] += tokens_count
82
 
83
  def get_performance_metrics():
84
+ global concurrent_requests
85
  with concurrent_requests_lock:
86
  current_concurrent = concurrent_requests
87
 
88
+ if not request_durations:
89
+ avg_request_time = 0
90
+ peak_request_time = 0
91
+ else:
92
+ avg_request_time = sum(request_durations) / len(request_durations)
93
+ peak_request_time = max(request_durations)
94
+
95
  now = datetime.now()
96
  one_minute_ago = now - timedelta(seconds=60)
97
  requests_last_minute = sum(1 for ts in request_timestamps if ts > one_minute_ago)
98
+
99
+ today = now.strftime("%Y-%m-%d")
100
+ today_requests = daily_requests.get(today, 0)
101
+ today_tokens = daily_tokens.get(today, 0)
102
+
103
  last_7_days = []
104
  for i in range(7):
105
  date = (now - timedelta(days=i)).strftime("%Y-%m-%d")
106
  last_7_days.append({
107
  "date": date,
108
  "requests": daily_requests.get(date, 0),
109
+ "tokens": daily_tokens.get(date, 0)
110
  })
111
+
112
  return {
113
  "avg_request_time_ms": avg_request_time * 1000,
114
  "peak_request_time_ms": peak_request_time * 1000,
115
  "requests_per_minute": requests_last_minute,
116
  "concurrent_requests": current_concurrent,
117
  "today_requests": today_requests,
118
+ "today_tokens": today_tokens,
119
  "last_7_days": last_7_days
120
  }
121
 
 
127
  def moderations():
128
  global concurrent_requests
129
 
 
 
 
 
130
  with concurrent_requests_lock:
131
  concurrent_requests += 1
132
 
133
  start_time = time.time()
134
+ total_tokens = 0
135
 
136
+ response = None
137
  try:
138
+ auth_header = request.headers.get('Authorization')
139
+ if not auth_header or not auth_header.startswith("Bearer "):
140
+ response = jsonify({"error": "Unauthorized"}), 401
141
+ return response
142
+
143
+ provided_api_key = auth_header.split(" ")[1]
144
+ if provided_api_key != API_KEY:
145
+ response = jsonify({"error": "Unauthorized"}), 401
146
+ return response
147
+
148
  data = request.get_json()
 
 
 
149
  raw_input = data.get('input')
150
+
151
  if raw_input is None:
152
+ response = jsonify({"error": "Invalid input, 'input' field is required"}), 400
153
+ return response
154
+
155
+ if isinstance(raw_input, str):
156
+ texts = [raw_input]
157
+ elif isinstance(raw_input, list):
158
+ texts = raw_input
159
+ else:
160
+ response = jsonify({"error": "Invalid input format, expected string or list of strings"}), 400
161
+ return response
162
+
163
+ if not texts:
164
+ response = jsonify({"error": "Input list cannot be empty"}), 400
165
+ return response
166
 
167
+ if len(texts) > 10:
168
+ response = jsonify({"error": "Too many input items. Maximum 10 allowed."}), 400
169
+ return response
170
+
171
  results = []
172
+ for text in texts:
173
+ if not isinstance(text, str):
174
+ if isinstance(text, dict) and 'type' in text and text['type'] == 'image_url':
175
+ image_data = text.get('image_url', {}).get('url', '')
176
+ if image_data.startswith('data:image'):
177
+ image_b64 = image_data.split(',')[1]
178
+ is_nsfw = classify_image_nsfw(image_b64)
179
+
180
+ results.append({
181
+ "flagged": is_nsfw,
182
+ "categories": {
183
+ "sexual": is_nsfw,
184
+ "sexual_explicit": is_nsfw
185
+ },
186
+ "category_scores": {
187
+ "sexual": 0.9 if is_nsfw else 0.1,
188
+ "sexual_explicit": 0.9 if is_nsfw else 0.1
189
+ }
190
+ })
191
+ continue
192
+ response = jsonify({"error": "Each input item must be a string or image object"}), 400
193
+ return response
194
+
195
+ if len(text.encode('utf-8')) > 300000:
196
+ response = jsonify({"error": "Each input item must have a maximum of 300k bytes."}), 400
197
+ return response
198
+
199
+ total_tokens += count_tokens(text)
200
+
201
+ predictions = detoxify_model.predict([text])
202
+ single_prediction = {key: value[0] for key, value in predictions.items()}
203
+ flagged, bool_categories, scores = transform_predictions(single_prediction)
204
+
205
+ results.append({
206
+ "flagged": flagged,
207
+ "categories": bool_categories,
208
+ "category_scores": scores,
209
+ })
210
 
 
 
 
211
  response_data = {
212
  "id": "modr-" + uuid.uuid4().hex[:24],
213
+ "model": "text-moderation-detoxify-multilingual",
214
+ "results": results
215
  }
216
 
217
+ response = jsonify(response_data)
218
+ return response
219
+
220
  except Exception as e:
221
  app.logger.error(f"An error occurred: {e}", exc_info=True)
222
+ response = jsonify({"error": "An internal server error occurred."}), 500
223
+ return response
224
  finally:
225
+ if response and (response[1] < 400 if isinstance(response, tuple) else response.status_code < 400):
226
+ track_request_metrics(start_time, total_tokens)
227
+
228
  with concurrent_requests_lock:
229
  concurrent_requests -= 1
230
 
231
  @app.route('/v1/metrics', methods=['GET'])
232
  def metrics():
233
  auth_header = request.headers.get('Authorization')
234
+ if not auth_header or not auth_header.startswith("Bearer "):
235
+ return jsonify({"error": "Unauthorized"}), 401
236
+
237
+ provided_api_key = auth_header.split(" ")[1]
238
+ if provided_api_key != API_KEY:
239
  return jsonify({"error": "Unauthorized"}), 401
240
 
241
  return jsonify(get_performance_metrics())
242
 
243
+ # Create the HTML template with Tailwind CSS
244
+ with open("templates/index.html", "w") as f:
245
+ f.write("""
246
+ <!DOCTYPE html>
247
+ <html lang="en">
248
+ <head>
249
+ <meta charset="UTF-8">
250
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
251
+ <title>AI Content Moderator</title>
252
+ <script src="https://cdn.tailwindcss.com"></script>
253
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
254
+ <style>
255
+ @keyframes float {
256
+ 0% { transform: translateY(0px); }
257
+ 50% { transform: translateY(-10px); }
258
+ 100% { transform: translateY(0px); }
259
+ }
260
+ @keyframes pulse-border {
261
+ 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
262
+ 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); }
263
+ 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
264
+ }
265
+ .float-animation {
266
+ animation: float 3s ease-in-out infinite;
267
+ }
268
+ .pulse-border {
269
+ animation: pulse-border 2s infinite;
270
+ }
271
+ .gradient-bg {
272
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
273
+ }
274
+ .glass-effect {
275
+ background: rgba(255, 255, 255, 0.1);
276
+ backdrop-filter: blur(10px);
277
+ border-radius: 10px;
278
+ border: 1px solid rgba(255, 255, 255, 0.2);
279
+ }
280
+ .safe-gradient {
281
+ background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
282
+ }
283
+ .unsafe-gradient {
284
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
285
+ }
286
+ </style>
287
+ </head>
288
+ <body class="min-h-screen gradient-bg text-white">
289
+ <div class="container mx-auto px-4 py-8">
290
+ <header class="text-center mb-12">
291
+ <div class="inline-block p-4 rounded-full glass-effect float-animation mb-6">
292
+ <i class="fas fa-shield-alt text-5xl text-white"></i>
293
+ </div>
294
+ <h1 class="text-4xl md:text-5xl font-bold mb-4">Smart Moderator</h1>
295
+ <p class="text-xl text-gray-200 max-w-2xl mx-auto">
296
+ Advanced, multilingual and multimodal content moderation API
297
+ </p>
298
+ </header>
299
+
300
+ <main class="max-w-6xl mx-auto">
301
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-12">
302
+ <!-- API Key Section -->
303
+ <div class="lg:col-span-1">
304
+ <div class="glass-effect p-6 rounded-xl h-full">
305
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
306
+ <i class="fas fa-key mr-2"></i> API Configuration
307
+ </h2>
308
+ <div class="mb-4">
309
+ <label class="block text-sm font-medium mb-2">API Key</label>
310
+ <div class="relative">
311
+ <input type="password" id="apiKey" placeholder="Enter your API key"
312
+ class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white">
313
+ <button id="toggleApiKey" class="absolute right-3 top-3 text-gray-300 hover:text-white">
314
+ <i class="fas fa-eye"></i>
315
+ </button>
316
+ </div>
317
+ </div>
318
+ <div class="mb-4">
319
+ <label class="block text-sm font-medium mb-2">Model</label>
320
+ <select id="modelSelect" class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white">
321
+ <option value="text-moderation-detoxify-multilingual" selected>Detoxify Multilingual</option>
322
+ </select>
323
+ </div>
324
+ <div class="mt-6">
325
+ <h3 class="text-lg font-semibold mb-2">API Usage</h3>
326
+ <div class="bg-black/20 p-4 rounded-lg text-sm font-mono">
327
+ <div class="mb-2">POST /v1/moderations</div>
328
+ <div class="mb-2">Site: https://nixaut-codelabs-smart-moderator.hf.space</div>
329
+ <div class="text-xs text-gray-300">curl -X POST -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" -d '{"input":"text to moderate"}' /v1/moderations</div>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </div>
334
+
335
+ <!-- Main Content Section -->
336
+ <div class="lg:col-span-2">
337
+ <div class="glass-effect p-6 rounded-xl">
338
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
339
+ <i class="fas fa-check-circle mr-2"></i> Content Analysis
340
+ </h2>
341
+
342
+ <!-- Tabs -->
343
+ <div class="flex border-b border-white/20 mb-6">
344
+ <button id="singleTab" class="px-4 py-2 font-medium border-b-2 border-indigo-400 text-indigo-300 tab-active">
345
+ Single Text
346
+ </button>
347
+ <button id="batchTab" class="px-4 py-2 font-medium border-b-2 border-transparent text-gray-300 hover:text-white">
348
+ Batch Processing
349
+ </button>
350
+ </div>
351
+
352
+ <!-- Single Text Tab -->
353
+ <div id="singleContent" class="tab-content">
354
+ <div class="mb-6">
355
+ <label class="block text-sm font-medium mb-2">Text to Analyze</label>
356
+ <textarea id="textInput" rows="6" placeholder="Enter any text in any language for content moderation analysis..."
357
+ class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white resize-none"></textarea>
358
+ </div>
359
+
360
+
361
+ <div class="flex space-x-4">
362
+ <button id="analyzeBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105 pulse-border">
363
+ <i class="fas fa-search mr-2"></i> Analyze Content
364
+ </button>
365
+ <button id="clearBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
366
+ <i class="fas fa-trash mr-2"></i> Clear
367
+ </button>
368
+ </div>
369
+ </div>
370
+
371
+ <!-- Batch Processing Tab -->
372
+ <div id="batchContent" class="tab-content hidden">
373
+ <div class="mb-6">
374
+ <label class="block text-sm font-medium mb-2">Texts to Analyze (one per line)</label>
375
+ <textarea id="batchTextInput" rows="8" placeholder="Enter multiple texts, one per line..."
376
+ class="w-full px-4 py-3 rounded-lg bg-white/10 border border-white/20 focus:outline-none focus:ring-2 focus:ring-indigo-400 text-white resize-none"></textarea>
377
+ </div>
378
+
379
+ <div class="flex space-x-4">
380
+ <button id="batchAnalyzeBtn" class="flex-1 bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105 pulse-border">
381
+ <i class="fas fa-layer-group mr-2"></i> Analyze Batch
382
+ </button>
383
+ <button id="batchClearBtn" class="bg-gray-600 hover:bg-gray-700 text-white font-bold py-3 px-6 rounded-lg transition duration-300">
384
+ <i class="fas fa-trash mr-2"></i> Clear
385
+ </button>
386
+ </div>
387
+ </div>
388
+
389
+ <!-- Results Section -->
390
+ <div id="resultsSection" class="mt-8 hidden">
391
+ <h3 class="text-xl font-bold mb-4 flex items-center">
392
+ <i class="fas fa-chart-bar mr-2"></i> Analysis Results
393
+ </h3>
394
+ <div id="resultsContainer" class="space-y-4">
395
+ <!-- Results will be dynamically inserted here -->
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Examples Section -->
403
+ <div class="glass-effect p-6 rounded-xl mb-12">
404
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
405
+ <i class="fas fa-lightbulb mr-2"></i> Example Prompts
406
+ </h2>
407
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
408
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
409
+ <p class="text-sm">"Hello, how are you today? I hope you're having a wonderful time!"</p>
410
+ </div>
411
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
412
+ <p class="text-sm">"I hate you and I will find you and hurt you badly."</p>
413
+ </div>
414
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
415
+ <p class="text-sm">"C'est une belle journée pour apprendre la programmation et l'intelligence artificielle."</p>
416
+ </div>
417
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
418
+ <p class="text-sm">"I can't take this anymore. I want to end everything and disappear forever."</p>
419
+ </div>
420
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
421
+ <p class="text-sm">"¡Hola! Me encanta aprender nuevos idiomas y conocer diferentes culturas."</p>
422
+ </div>
423
+ <div class="example-card bg-white/10 p-4 rounded-lg cursor-pointer hover:bg-white/20 transition duration-300">
424
+ <p class="text-sm">"You're absolutely worthless and nobody will ever love someone like you."</p>
425
+ </div>
426
+ </div>
427
+ </div>
428
+
429
+ <!-- Information Section -->
430
+ <div class="glass-effect p-6 rounded-xl">
431
+ <h2 class="text-2xl font-bold mb-4 flex items-center">
432
+ <i class="fas fa-info-circle mr-2"></i> About This Tool
433
+ </h2>
434
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
435
+ <div class="text-center">
436
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
437
+ <i class="fas fa-globe text-2xl text-indigo-300"></i>
438
+ </div>
439
+ <h3 class="text-lg font-semibold mb-2">Multilingual</h3>
440
+ <p class="text-gray-300">Supports content analysis in multiple languages with high accuracy.</p>
441
+ </div>
442
+ <div class="text-center">
443
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
444
+ <i class="fas fa-bolt text-2xl text-indigo-300"></i>
445
+ </div>
446
+ <h3 class="text-lg font-semibold mb-2">Fast Processing</h3>
447
+ <p class="text-gray-300">Optimized model for quick content analysis with real-time results.</p>
448
+ </div>
449
+ <div class="text-center">
450
+ <div class="inline-block p-3 rounded-full bg-indigo-500/20 mb-3">
451
+ <i class="fas fa-shield-alt text-2xl text-indigo-300"></i>
452
+ </div>
453
+ <h3 class="text-lg font-semibold mb-2">Secure</h3>
454
+ <p class="text-gray-300">API key authentication ensures your requests remain secure and private.</p>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ </main>
459
+
460
+ <footer class="mt-12 text-center text-gray-300">
461
+ <p>© 2025 Smart Moderator. All rights reserved.</p>
462
+ </footer>
463
+ </div>
464
+
465
+ <!-- Loading Modal -->
466
+ <div id="loadingModal" class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 hidden">
467
+ <div class="glass-effect p-8 rounded-xl max-w-md w-full mx-4 text-center">
468
+ <div class="mb-4">
469
+ <div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
470
+ </div>
471
+ <h3 class="text-xl font-bold mb-2">Analyzing Content</h3>
472
+ <p class="text-gray-300">Please wait while we process your request...</p>
473
+ </div>
474
+ </div>
475
+
476
+ <script>
477
+ // DOM Elements
478
+ const singleTab = document.getElementById('singleTab');
479
+ const batchTab = document.getElementById('batchTab');
480
+ const singleContent = document.getElementById('singleContent');
481
+ const batchContent = document.getElementById('batchContent');
482
+ const apiKeyInput = document.getElementById('apiKey');
483
+ const toggleApiKeyBtn = document.getElementById('toggleApiKey');
484
+ const textInput = document.getElementById('textInput');
485
+ const batchTextInput = document.getElementById('batchTextInput');
486
+ const analyzeBtn = document.getElementById('analyzeBtn');
487
+ const batchAnalyzeBtn = document.getElementById('batchAnalyzeBtn');
488
+ const clearBtn = document.getElementById('clearBtn');
489
+ const batchClearBtn = document.getElementById('batchClearBtn');
490
+ const resultsSection = document.getElementById('resultsSection');
491
+ const resultsContainer = document.getElementById('resultsContainer');
492
+ const loadingModal = document.getElementById('loadingModal');
493
+ const exampleCards = document.querySelectorAll('.example-card');
494
+
495
+ // Tab switching
496
+ singleTab.addEventListener('click', () => {
497
+ singleTab.classList.add('border-indigo-400', 'text-indigo-300');
498
+ singleTab.classList.remove('border-transparent', 'text-gray-300');
499
+ batchTab.classList.add('border-transparent', 'text-gray-300');
500
+ batchTab.classList.remove('border-indigo-400', 'text-indigo-300');
501
+ singleContent.classList.remove('hidden');
502
+ batchContent.classList.add('hidden');
503
+ });
504
+
505
+ batchTab.addEventListener('click', () => {
506
+ batchTab.classList.add('border-indigo-400', 'text-indigo-300');
507
+ batchTab.classList.remove('border-transparent', 'text-gray-300');
508
+ singleTab.classList.add('border-transparent', 'text-gray-300');
509
+ singleTab.classList.remove('border-indigo-400', 'text-indigo-300');
510
+ batchContent.classList.remove('hidden');
511
+ singleContent.classList.add('hidden');
512
+ });
513
+
514
+ // Toggle API key visibility
515
+ toggleApiKeyBtn.addEventListener('click', () => {
516
+ const type = apiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
517
+ apiKeyInput.setAttribute('type', type);
518
+ toggleApiKeyBtn.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
519
+ });
520
+
521
+ // Slider value updates
522
+ maxTokensSlider.addEventListener('input', () => {
523
+ maxTokensValue.textContent = maxTokensSlider.value;
524
+ });
525
+
526
+ temperatureSlider.addEventListener('input', () => {
527
+ temperatureValue.textContent = temperatureSlider.value;
528
+ });
529
+
530
+ topPSlider.addEventListener('input', () => {
531
+ topPValue.textContent = topPSlider.value;
532
+ });
533
+
534
+ // Example cards
535
+ exampleCards.forEach(card => {
536
+ card.addEventListener('click', () => {
537
+ textInput.value = card.querySelector('p').textContent;
538
+ });
539
+ });
540
+
541
+ // Clear buttons
542
+ clearBtn.addEventListener('click', () => {
543
+ textInput.value = '';
544
+ resultsSection.classList.add('hidden');
545
+ });
546
+
547
+ batchClearBtn.addEventListener('click', () => {
548
+ batchTextInput.value = '';
549
+ resultsSection.classList.add('hidden');
550
+ });
551
+
552
+ // Analyze button
553
+ analyzeBtn.addEventListener('click', async () => {
554
+ const text = textInput.value.trim();
555
+ if (!text) {
556
+ showNotification('Please enter text to analyze', 'error');
557
+ return;
558
+ }
559
+
560
+ const apiKey = apiKeyInput.value.trim();
561
+ if (!apiKey) {
562
+ showNotification('Please enter your API key', 'error');
563
+ return;
564
+ }
565
+
566
+ showLoading(true);
567
+
568
+ try {
569
+ const response = await fetch('/v1/moderations', {
570
+ method: 'POST',
571
+ headers: {
572
+ 'Content-Type': 'application/json',
573
+ 'Authorization': `Bearer ${apiKey}`
574
+ },
575
+ body: JSON.stringify({
576
+ input: text,
577
+ model: document.getElementById('modelSelect').value
578
+ })
579
+ });
580
+
581
+ if (!response.ok) {
582
+ const errorData = await response.json();
583
+ throw new Error(errorData.detail || 'An error occurred');
584
+ }
585
+
586
+ const data = await response.json();
587
+ displayResults([data.results[0]]);
588
+ } catch (error) {
589
+ showNotification(`Error: ${error.message}`, 'error');
590
+ } finally {
591
+ showLoading(false);
592
+ }
593
+ });
594
+
595
+ // Batch analyze button
596
+ batchAnalyzeBtn.addEventListener('click', async () => {
597
+ const texts = batchTextInput.value.trim().split('\\n').filter(text => text.trim());
598
+ if (texts.length === 0) {
599
+ showNotification('Please enter at least one text to analyze', 'error');
600
+ return;
601
+ }
602
+
603
+ const apiKey = apiKeyInput.value.trim();
604
+ if (!apiKey) {
605
+ showNotification('Please enter your API key', 'error');
606
+ return;
607
+ }
608
+
609
+ showLoading(true);
610
+
611
+ try {
612
+ const response = await fetch('/v1/moderations', {
613
+ method: 'POST',
614
+ headers: {
615
+ 'Content-Type': 'application/json',
616
+ 'Authorization': `Bearer ${apiKey}`
617
+ },
618
+ body: JSON.stringify({
619
+ input: texts,
620
+ model: document.getElementById('modelSelect').value
621
+ })
622
+ });
623
+
624
+ if (!response.ok) {
625
+ const errorData = await response.json();
626
+ throw new Error(errorData.detail || 'An error occurred');
627
+ }
628
+
629
+ const data = await response.json();
630
+ displayResults(data.results);
631
+ } catch (error) {
632
+ showNotification(`Error: ${error.message}`, 'error');
633
+ } finally {
634
+ showLoading(false);
635
+ }
636
+ });
637
+
638
+ // Display results
639
+ function displayResults(results) {
640
+ resultsContainer.innerHTML = '';
641
+
642
+ results.forEach((result, index) => {
643
+ const isFlagged = result.flagged;
644
+ const cardClass = isFlagged ? 'unsafe-gradient' : 'safe-gradient';
645
+ const icon = isFlagged ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle';
646
+ const statusText = isFlagged ? 'UNSAFE' : 'SAFE';
647
+ const statusDesc = isFlagged ?
648
+ 'Content may contain inappropriate or harmful material.' :
649
+ 'Content appears to be safe and appropriate.';
650
+
651
+ const categories = Object.entries(result.categories)
652
+ .filter(([_, value]) => value)
653
+ .map(([key, _]) => key.replace('/', ' '))
654
+ .join(', ');
655
+
656
+ const resultCard = document.createElement('div');
657
+ resultCard.className = `p-6 rounded-xl text-white ${cardClass} shadow-lg`;
658
+ resultCard.innerHTML = `
659
+ <div class="flex items-start">
660
+ <div class="mr-4 mt-1">
661
+ <i class="${icon} text-3xl"></i>
662
+ </div>
663
+ <div class="flex-1">
664
+ <div class="flex justify-between items-start mb-2">
665
+ <h3 class="text-xl font-bold">${statusText}</h3>
666
+ <span class="text-sm bg-black/20 px-2 py-1 rounded">Result ${index + 1}</span>
667
+ </div>
668
+ <p class="mb-4">${statusDesc}</p>
669
+ <div class="bg-black/20 p-4 rounded-lg mb-4">
670
+ <p class="text-sm font-mono break-words">${result.text}</p>
671
+ </div>
672
+ ${isFlagged ? `
673
+ <div class="mb-3">
674
+ <h4 class="font-semibold mb-1">Flagged Categories:</h4>
675
+ <p class="text-sm">${categories}</p>
676
+ </div>
677
+ ` : ''}
678
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
679
+ ${Object.entries(result.category_scores).map(([category, score]) => `
680
+ <div class="bg-black/20 p-2 rounded">
681
+ <div class="font-medium">${category.replace('/', ' ')}</div>
682
+ <div class="w-full bg-gray-700 rounded-full h-1.5 mt-1">
683
+ <div class="bg-white h-1.5 rounded-full" style="width: ${score * 100}%"></div>
684
+ </div>
685
+ <div class="text-right mt-1">${(score * 100).toFixed(0)}%</div>
686
+ </div>
687
+ `).join('')}
688
+ </div>
689
+ </div>
690
+ </div>
691
+ `;
692
+
693
+ resultsContainer.appendChild(resultCard);
694
+ });
695
+
696
+ resultsSection.classList.remove('hidden');
697
+ resultsSection.scrollIntoView({ behavior: 'smooth' });
698
+ }
699
+
700
+ // Show/hide loading modal
701
+ function showLoading(show) {
702
+ if (show) {
703
+ loadingModal.classList.remove('hidden');
704
+ } else {
705
+ loadingModal.classList.add('hidden');
706
+ }
707
+ }
708
+
709
+ // Show notification
710
+ function showNotification(message, type = 'info') {
711
+ const notification = document.createElement('div');
712
+ notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
713
+ type === 'error' ? 'bg-red-500' : 'bg-indigo-500'
714
+ } text-white`;
715
+ notification.innerHTML = `
716
+ <div class="flex items-center">
717
+ <i class="fas ${type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i>
718
+ <span>${message}</span>
719
+ </div>
720
+ `;
721
+
722
+ document.body.appendChild(notification);
723
+
724
+ setTimeout(() => {
725
+ notification.style.opacity = '0';
726
+ notification.style.transition = 'opacity 0.5s';
727
+ setTimeout(() => {
728
+ document.body.removeChild(notification);
729
+ }, 500);
730
+ }, 3000);
731
+ }
732
+ </script>
733
+ </body>
734
+ </html>
735
+ """)
736
+
737
+ def create_directories_and_files():
738
  os.makedirs('templates', exist_ok=True)
739
  os.makedirs('static', exist_ok=True)
740
 
741
+ index_path = os.path.join('templates', 'index.html')
742
+ if not os.path.exists(index_path):
743
+ with open(index_path, 'w', encoding='utf-8') as f:
744
+ f.write('''<!DOCTYPE html>
745
  <html lang="en">
746
  <head>
747
  <meta charset="UTF-8">
748
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
749
+ <title>Smart Moderator</title>
750
  <script src="https://cdn.tailwindcss.com"></script>
751
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
752
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
753
  <script>
754
+ tailwind.config = {
755
+ darkMode: 'class',
756
+ theme: {
757
+ extend: {
758
+ colors: {
759
+ primary: {
760
+ 50: '#eff6ff', 100: '#dbeafe', 200: '#bfdbfe', 300: '#93c5fd', 400: '#60a5fa',
761
+ 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8', 800: '#1e40af', 900: '#1e3a8a',
762
+ }
763
+ }
764
+ }
765
+ }
766
+ }
767
  </script>
768
  <style>
769
  .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
770
  .dark .gradient-bg { background: linear-gradient(135deg, #1e3a8a 0%, #4c1d95 100%); }
771
+ .glass-effect {
772
+ background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px);
773
+ border: 1px solid rgba(255, 255, 255, 0.2);
774
+ }
775
+ .dark .glass-effect { background: rgba(30, 41, 59, 0.5); border: 1px solid rgba(100, 116, 139, 0.3); }
776
+ .category-card { transition: all 0.3s ease; }
777
+ .category-card:hover { transform: translateY(-5px); }
778
  .loading-spinner { border-top-color: #3b82f6; animation: spinner 1.5s linear infinite; }
779
  @keyframes spinner { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
780
  </style>
 
784
  <div class="container mx-auto px-4 py-6 flex justify-between items-center">
785
  <div class="flex items-center space-x-3">
786
  <div class="w-10 h-10 rounded-full bg-white flex items-center justify-center">
787
+ <i class="fas fa-shield-alt text-primary-600 text-xl"></i>
788
  </div>
789
  <h1 class="text-2xl font-bold">Smart Moderator</h1>
790
  </div>
791
+ <div class="flex items-center space-x-4">
792
+ <button id="refreshMetrics" class="glass-effect px-4 py-2 rounded-lg hover:bg-white/20 transition">
793
+ <i class="fas fa-sync-alt mr-2"></i>Refresh Metrics
794
+ </button>
795
+ <button id="darkModeToggle" class="glass-effect p-2 rounded-lg hover:bg-white/20 transition">
796
  <i class="fas fa-moon dark:hidden"></i>
797
  <i class="fas fa-sun hidden dark:inline"></i>
798
  </button>
 
802
 
803
  <main class="container mx-auto px-4 py-8">
804
  <section class="mb-12">
805
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
806
+ <i class="fas fa-chart-line mr-3 text-primary-600"></i>
807
+ Performance Metrics
808
+ </h2>
809
+
810
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
811
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
812
+ <div class="flex items-center justify-between">
813
+ <div>
814
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Avg. Response (last 100)</p>
815
+ <p class="text-2xl font-bold" id="avgResponseTime">0ms</p>
816
+ </div>
817
+ <div class="w-12 h-12 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
818
+ <i class="fas fa-clock text-primary-600 dark:text-primary-400"></i>
819
+ </div>
820
+ </div>
821
+ </div>
822
+
823
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
824
+ <div class="flex items-center justify-between">
825
+ <div>
826
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Requests / Minute</p>
827
+ <p class="text-2xl font-bold" id="requestsPerMinute">0</p>
828
+ </div>
829
+ <div class="w-12 h-12 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
830
+ <i class="fas fa-tachometer-alt text-green-600 dark:text-green-400"></i>
831
+ </div>
832
+ </div>
833
+ </div>
834
+
835
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
836
+ <div class="flex items-center justify-between">
837
+ <div>
838
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Peak Response (last 100)</p>
839
+ <p class="text-2xl font-bold" id="peakResponseTime">0ms</p>
840
+ </div>
841
+ <div class="w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center">
842
+ <i class="fas fa-exclamation-triangle text-red-600 dark:text-red-400"></i>
843
+ </div>
844
+ </div>
845
+ </div>
846
+
847
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
848
+ <div class="flex items-center justify-between">
849
+ <div>
850
+ <p class="text-gray-500 dark:text-gray-400 text-sm">Today's Requests</p>
851
+ <p class="text-2xl font-bold" id="todayRequests">0</p>
852
+ </div>
853
+ <div class="w-12 h-12 rounded-full bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
854
+ <i class="fas fa-list-ol text-yellow-600 dark:text-yellow-400"></i>
855
+ </div>
856
+ </div>
857
+ </div>
858
  </div>
859
+
860
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
861
  <h3 class="text-lg font-semibold mb-4">Last 7 Days Activity</h3>
862
+ <div class="h-64">
863
+ <canvas id="activityChart"></canvas>
864
+ </div>
865
  </div>
866
  </section>
867
 
868
  <section class="mb-12">
869
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
870
+ <i class="fas fa-code mr-3 text-primary-600"></i>
871
+ API Tester
872
+ </h2>
873
+
874
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
875
  <form id="apiTestForm">
876
+ <div class="mb-6">
877
+ <label class="block text-sm font-medium mb-2" for="apiKey">API Key</label>
878
+ <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">
879
+ </div>
880
+
881
+ <div class="mb-6">
882
+ <label class="block text-sm font-medium mb-2">Text Inputs</label>
883
+ <div id="textInputsContainer">
884
+ <div class="text-input-group mb-4">
885
+ <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>
886
+ <button type="button" class="remove-input mt-2 text-red-500 hover:text-red-700 hidden">
887
+ <i class="fas fa-trash-alt mr-1"></i> Remove
888
+ </button>
889
+ </div>
890
+ </div>
891
+ <button type="button" id="addTextInput" class="mt-2 text-primary-600 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300">
892
+ <i class="fas fa-plus-circle mr-1"></i> Add another text input
893
+ </button>
894
+ </div>
895
+
896
+ <div class="flex justify-between items-center">
897
+ <div>
898
+ <button type="submit" id="analyzeBtn" class="bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-6 rounded-lg transition">
899
+ <i class="fas fa-search mr-2"></i> Analyze Text
900
+ </button>
901
+ <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">
902
+ <i class="fas fa-eraser mr-2"></i> Clear
903
+ </button>
904
+ </div>
905
+ <div class="text-sm text-gray-500 dark:text-gray-400">
906
+ <i class="fas fa-info-circle mr-1"></i> Maximum 10 text inputs allowed
907
+ </div>
908
+ </div>
909
  </form>
910
  </div>
911
  </section>
912
 
913
  <section id="resultsSection" class="hidden">
914
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
915
+ <i class="fas fa-clipboard-check mr-3 text-primary-600"></i>
916
+ Analysis Results
917
+ </h2>
918
+
919
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 mb-6">
920
+ <div class="flex justify-between items-center mb-4">
921
+ <h3 class="text-lg font-semibold">Summary</h3>
922
+ <div class="text-sm text-gray-500 dark:text-gray-400">
923
+ <i class="fas fa-stopwatch mr-1"></i> Round-trip time: <span id="responseTime">0ms</span>
924
+ </div>
925
+ </div>
926
+
927
+ <div id="resultsContainer" class="space-y-6">
928
+ </div>
929
+ </div>
930
  </section>
931
+
932
  <section>
933
+ <h2 class="text-2xl font-bold mb-6 flex items-center">
934
+ <i class="fas fa-book mr-3 text-primary-600"></i>
935
+ API Documentation
936
+ </h2>
937
  <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
938
+ <h3 class="text-lg font-semibold mb-4">Endpoint</h3>
939
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">POST /v1/moderations</div>
940
+ <h3 class="text-lg font-semibold mb-4">Site URL</h3>
941
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 font-mono text-sm">https://nixaut-codelabs-smart-moderator.hf.space</div>
942
+ <h3 class="text-lg font-semibold mb-4">Request Body</h3>
943
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg mb-6 overflow-x-auto">
944
+ <pre class="text-sm"><code>{
945
+ "input": "Text to moderate"
946
+ }</code></pre>
947
+ </div>
948
+ <h3 class="text-lg font-semibold mb-4">Response</h3>
949
+ <div class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
950
+ <pre class="text-sm"><code>{
951
+ "id": "modr-1234567890abcdef",
952
+ "model": "text-moderation-detoxify-multilingual",
953
+ "results": [
954
+ {
955
+ "flagged": true,
956
+ "categories": {
957
+ "toxicity": true,
958
+ "severe_toxicity": false,
959
+ /* ... other categories */
960
+ },
961
+ "category_scores": {
962
+ "toxicity": 0.95,
963
+ "severe_toxicity": 0.1,
964
+ /* ... other scores */
965
+ }
966
+ }
967
+ ]
968
+ }</code></pre>
969
  </div>
970
  </div>
971
  </section>
972
  </main>
973
+
974
  <footer class="bg-gray-100 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-12">
975
+ <div class="container mx-auto px-4 py-6 text-center text-gray-600 dark:text-gray-400">
976
+ 2025 Smart Moderator. All rights reserved.
977
  </div>
978
  </footer>
979
+
980
  <script>
981
  const darkModeToggle = document.getElementById('darkModeToggle');
982
+ const html = document.documentElement;
983
+
984
  if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
985
+ html.classList.add('dark');
986
  }
987
+
988
  darkModeToggle.addEventListener('click', () => {
989
+ html.classList.toggle('dark');
990
+ localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
991
  });
992
 
993
  let activityChart;
994
+
995
+ function initActivityChart() {
996
+ if (activityChart) { activityChart.destroy(); }
997
+ const ctx = document.getElementById('activityChart').getContext('2d');
998
+ const isDarkMode = document.documentElement.classList.contains('dark');
999
+ const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
1000
+ const textColor = isDarkMode ? '#e5e7eb' : '#374151';
1001
+
1002
+ activityChart = new Chart(ctx, {
1003
+ type: 'bar',
1004
+ data: { labels: [], datasets: [
1005
+ { label: 'Requests', data: [], backgroundColor: 'rgba(59, 130, 246, 0.6)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1 },
1006
+ { label: 'Tokens', data: [], backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgba(16, 185, 129, 1)', borderWidth: 1, yAxisID: 'y1' }
1007
+ ]},
1008
+ options: {
1009
+ responsive: true, maintainAspectRatio: false,
1010
+ scales: {
1011
+ y: { beginAtZero: true, position: 'left', title: { display: true, text: 'Requests', color: textColor }, ticks: { color: textColor }, grid: { color: gridColor } },
1012
+ y1: { beginAtZero: true, position: 'right', title: { display: true, text: 'Tokens', color: textColor }, ticks: { color: textColor }, grid: { drawOnChartArea: false } }
1013
+ },
1014
+ plugins: { legend: { labels: { color: textColor } } }
1015
+ }
1016
+ });
1017
+ }
1018
+
1019
  async function fetchMetrics() {
1020
+ const apiKey = document.getElementById('apiKey').value || 'temp-key-for-metrics';
1021
  try {
1022
+ const response = await fetch('/v1/metrics', {
1023
+ headers: { 'Authorization': 'Bearer ' + apiKey }
1024
+ });
1025
+ if (!response.ok) {
1026
+ const error = await response.json();
1027
+ console.error('Failed to fetch metrics:', error.error);
1028
+ if(response.status === 401) {
1029
+ }
1030
+ return;
 
 
 
 
 
1031
  }
1032
+ const data = await response.json();
1033
+ updateMetricsDisplay(data);
1034
+ } catch (error) {
1035
+ console.error('Error fetching metrics:', error);
1036
+ }
1037
  }
1038
 
1039
+ function updateMetricsDisplay(data) {
1040
+ document.getElementById('avgResponseTime').textContent = data.avg_request_time_ms.toFixed(0) + 'ms';
1041
+ document.getElementById('peakResponseTime').textContent = data.peak_request_time_ms.toFixed(0) + 'ms';
1042
+ document.getElementById('requestsPerMinute').textContent = data.requests_per_minute;
1043
+ document.getElementById('todayRequests').textContent = data.today_requests.toLocaleString();
1044
+
1045
+ if (activityChart) {
1046
+ const labels = data.last_7_days.map(d => new Date(d.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).reverse();
1047
+ const requests = data.last_7_days.map(d => d.requests).reverse();
1048
+ const tokens = data.last_7_days.map(d => d.tokens).reverse();
1049
+
1050
+ activityChart.data.labels = labels;
1051
+ activityChart.data.datasets[0].data = requests;
1052
+ activityChart.data.datasets[1].data = tokens;
1053
+ activityChart.update();
1054
+ }
1055
  }
1056
 
1057
+ document.getElementById('addTextInput').addEventListener('click', () => {
1058
+ const container = document.getElementById('textInputsContainer');
1059
+ if (container.children.length >= 10) {
1060
+ alert('Maximum 10 text inputs allowed');
1061
+ return;
1062
+ }
1063
+ const newGroup = container.firstElementChild.cloneNode(true);
1064
+ newGroup.querySelector('textarea').value = '';
1065
+ newGroup.querySelector('.remove-input').classList.remove('hidden');
1066
+ container.appendChild(newGroup);
1067
+ updateRemoveButtons();
1068
+ });
1069
+
1070
+ document.getElementById('textInputsContainer').addEventListener('click', function(e) {
1071
+ if (e.target.closest('.remove-input')) {
1072
+ e.target.closest('.text-input-group').remove();
1073
+ updateRemoveButtons();
1074
+ }
1075
+ });
1076
+
1077
+ function updateRemoveButtons() {
1078
+ const groups = document.querySelectorAll('.text-input-group');
1079
+ groups.forEach(group => {
1080
+ group.querySelector('.remove-input').classList.toggle('hidden', groups.length <= 1);
1081
+ });
1082
+ }
1083
+
1084
+ document.getElementById('clearBtn').addEventListener('click', () => {
1085
+ document.getElementById('apiTestForm').reset();
1086
+ const container = document.getElementById('textInputsContainer');
1087
+ container.innerHTML = container.firstElementChild.outerHTML;
1088
+ container.querySelector('textarea').value = '';
1089
+ updateRemoveButtons();
1090
+ document.getElementById('resultsSection').classList.add('hidden');
1091
+ });
1092
+
1093
  document.getElementById('apiTestForm').addEventListener('submit', async (e) => {
1094
  e.preventDefault();
1095
+
1096
  const apiKey = document.getElementById('apiKey').value;
1097
+ if (!apiKey) { alert('Please enter your API key'); return; }
1098
+
1099
+ const texts = Array.from(document.querySelectorAll('#textInputsContainer textarea'))
1100
+ .map(t => t.value.trim()).filter(Boolean);
1101
+ if (texts.length === 0) { alert('Please enter at least one text to analyze'); return; }
1102
 
 
 
 
 
 
 
1103
  const analyzeBtn = document.getElementById('analyzeBtn');
1104
+ const originalBtnContent = analyzeBtn.innerHTML;
1105
+ 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...';
1106
  analyzeBtn.disabled = true;
1107
+
1108
+ const startTime = Date.now();
1109
+
1110
  try {
1111
  const response = await fetch('/v1/moderations', {
1112
  method: 'POST',
1113
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
1114
+ body: JSON.stringify({ input: texts.length === 1 ? texts[0] : texts })
1115
  });
1116
+
1117
+ const responseTime = Date.now() - startTime;
1118
+
1119
  const data = await response.json();
1120
+ if (!response.ok) throw new Error(data.error || 'Failed to analyze text');
1121
+
1122
+ displayResults(data, responseTime, texts);
1123
  fetchMetrics();
1124
+
1125
  } catch (error) {
1126
+ alert('Error: ' + error.message);
1127
  } finally {
1128
+ analyzeBtn.innerHTML = originalBtnContent;
1129
  analyzeBtn.disabled = false;
 
1130
  }
1131
  });
1132
 
1133
+ function displayResults(data, responseTime, texts) {
1134
+ const resultsSection = document.getElementById('resultsSection');
1135
  const resultsContainer = document.getElementById('resultsContainer');
1136
+
1137
+ document.getElementById('responseTime').textContent = responseTime + 'ms';
1138
  resultsContainer.innerHTML = '';
1139
 
1140
+ data.results.forEach((result, index) => {
1141
+ const resultCard = document.createElement('div');
1142
+ resultCard.className = 'border border-gray-200 dark:border-gray-700 rounded-lg p-4';
1143
+
1144
+ const flaggedBadge = result.flagged
1145
+ ? '<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>'
1146
+ : '<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>';
1147
+
1148
+ let categoriesHtml = Object.entries(result.category_scores).map(([category, score]) => {
1149
+ const isFlagged = result.categories[category];
1150
+ const categoryClass = isFlagged ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400';
1151
+ 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';
1152
+ return `
1153
+ <div class="flex justify-between items-center py-2 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
1154
+ <span class="font-medium capitalize">${category.replace(/_/g, ' ')}</span>
1155
+ <div class="flex items-center">
1156
+ <span class="text-sm ${scoreClass} font-mono">${score.toFixed(4)}</span>
1157
+ </div>
1158
+ </div>
1159
+ `;
1160
+ }).join('');
1161
+
1162
+ resultCard.innerHTML = `
1163
+ <div class="flex justify-between items-start mb-3">
1164
+ <h4 class="text-lg font-semibold">Input ${index + 1}</h4>
1165
+ ${flaggedBadge}
1166
+ </div>
1167
+ <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>
1168
+ <div class="category-card">
1169
+ <h5 class="font-medium mb-2">Category Scores</h5>
1170
+ <div class="bg-white dark:bg-gray-800/50 rounded-lg p-2">
1171
+ ${categoriesHtml}
1172
+ </div>
1173
+ </div>
1174
+ `;
1175
+ resultsContainer.appendChild(resultCard);
1176
+ });
1177
+
1178
+ resultsSection.classList.remove('hidden');
1179
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
1180
  }
1181
 
1182
  document.addEventListener('DOMContentLoaded', () => {
1183
+ initActivityChart();
1184
+ document.getElementById('refreshMetrics').addEventListener('click', fetchMetrics);
1185
  fetchMetrics();
1186
+ setInterval(fetchMetrics, 15000);
1187
  });
1188
  </script>
1189
  </body>
1190
  </html>
1191
+ ''')
 
 
 
 
1192
 
1193
  if __name__ == '__main__':
1194
+ create_directories_and_files()
1195
+ port = int(os.getenv('PORT', 7860))
1196
+ app.run(host='0.0.0.0', port=port, debug=True, use_reloader=False)