ariansyahdedy commited on
Commit
a23cfdb
·
0 Parent(s):

Initial Commit

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ venv
2
+ __pycache__/
3
+ *.pyc
4
+ app/client_secret.json
5
+ app/client_secrets.json
6
+ .env
7
+ secret.json
8
+ test*.py
9
+ client_secret.json
10
+ app/main_backup.py
11
+ app/main*.py
12
+ test.json
13
+ draft.py
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Copy requirements and install them
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ # Copy the rest of the app (including app.py and templates folder)
11
+ COPY . .
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Gambling Comment Filter
3
+ emoji: 🎲
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: A PoC filter for detecting gambling-related comments
10
+ ---
11
+
12
+ # Gambling Comment Filter
13
+ A high-performance filter for detecting online gambling-related comments. This project is built using FastAPI and is designed to be deployed on Hugging Face Spaces. It uses robust Unicode normalization (via unidecode and a custom visual mapping) and dynamic rule management to catch obfuscated gambling content in comments.
14
+
15
+ ## Features
16
+ - **Robust Text Normalization:** Converts fancy or obfuscated Unicode characters (bold, italic, fullwidth, Cyrillic/Greek lookalikes) into plain ASCII.
17
+ - **Dynamic Rule Management:** Add or update filtering rules (platform names, gambling terms, safe indicators, gambling contexts, ambiguous terms) on the fly using a web interface.
18
+ - **File Upload Support:** Process comments in bulk by uploading CSV or Excel files.
19
+ - **Score-Based Classification:** Uses a scoring algorithm to determine if a comment is gambling-related based on multiple signals.
20
+ - **Hugging Face Spaces Ready:** Deploy your project easily with a Dockerfile and run it as a Hugging Face Space.
21
+
22
+ ## Project Structure
23
+ ```
24
+ gambling-comment-filter/
25
+ ├── app.py # Main FastAPI application with filtering logic and endpoints
26
+ ├── requirements.txt # Python dependencies
27
+ ├── Dockerfile # Docker configuration for deployment on Hugging Face Spaces
28
+ └── templates/
29
+ └── index.html # HTML template for the web interface
30
+ ```
31
+
32
+ ## Requirements
33
+ - Python 3.9+
34
+ - [FastAPI](https://fastapi.tiangolo.com/)
35
+ - [Uvicorn](https://www.uvicorn.org/)
36
+ - [Jinja2](https://palletsprojects.com/p/jinja/)
37
+ - [Pandas](https://pandas.pydata.org/)
38
+ - [openpyxl](https://openpyxl.readthedocs.io/en/stable/)
39
+ - [unidecode](https://pypi.org/project/Unidecode/)
40
+
41
+ ## Setup and Local Testing
42
+ 1. **Clone the Repository**
43
+ ```bash
44
+ git clone https://huggingface.co/spaces/ariansyahdedy/gambling-comment-filter
45
+ cd gambling-comment-filter
46
+ ```
47
+
48
+ 2. **Create a Virtual Environment and Install Dependencies**
49
+ ```bash
50
+ python -m venv venv
51
+ source venv/bin/activate # On Windows use: venv\Scripts\activate
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ 3. **Run the Application**
56
+ ```bash
57
+ uvicorn app:app --reload
58
+ ```
59
+
60
+ 4. **Access the Web Interface**
61
+ Open your browser and visit http://localhost:8000
62
+
63
+ ## Deployment on Hugging Face Spaces
64
+ 1. **Create a New Space**
65
+ Go to Hugging Face Spaces and create a new Space using the Docker runtime.
66
+
67
+ 2. **Push Your Local Project to the Space**
68
+ ```bash
69
+ cd path/to/gambling-comment-filter
70
+ git init # if not already a git repo
71
+ git add .
72
+ git commit -m "Initial commit for Gambling Comment Filter"
73
+ git remote add hf https://huggingface.co/spaces/ariansyahdedy/gambling-comment-filter
74
+ git push hf main
75
+ ```
76
+ The Space will automatically build and deploy your project.
77
+
78
+ ## Customization
79
+ * **Updating Rules:** Use the web interface to add new rules via the `/add_rule` endpoint.
80
+ * **Visual Mapping:** The `_robust_normalize` function uses a `VISUAL_MAP` dictionary to convert fancy characters into plain ASCII. You can update this mapping directly in `app.py` or add new entries through the `/add_visual_char` endpoint.
81
+ * **Scoring:** Adjust the scoring logic in `is_gambling_comment` if you want to tweak the sensitivity.
82
+
83
+ ## License
84
+ This project is licensed under the MIT License. See the LICENSE file for details.
85
+
86
+ ## Contributing
87
+ Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes.
requirements.txt ADDED
Binary file (1.91 kB). View file
 
static/css/styles.css ADDED
File without changes
static/js/app.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Modify the Refresh Comments button event listener
2
+ document.getElementById('refreshCommentsBtn').addEventListener('click', function() {
3
+ const videoId = "{{ video.id }}";
4
+ window.location.href = `/refresh_comments/${videoId}`;
5
+ });
6
+
7
+ // Modify the Keep Comment button handler
8
+ document.querySelectorAll('.keep-comment-btn').forEach(button => {
9
+ button.addEventListener('click', async function() {
10
+ const commentId = this.getAttribute('data-comment-id');
11
+ const videoId = "{{ video.id }}";
12
+
13
+ try {
14
+ const response = await fetch(`/api/comments/keep/${commentId}?video_id=${videoId}`, {
15
+ method: 'POST'
16
+ });
17
+ const data = await response.json();
18
+
19
+ if (data.success) {
20
+ // Remove from flagged comments
21
+ const commentCard = this.closest('.comment-card');
22
+ commentCard.remove();
23
+
24
+ // Update flagged comments count
25
+ const flaggedCommentsCount = document.querySelector('h2 span');
26
+ const currentCount = parseInt(flaggedCommentsCount.textContent.replace(/[()]/g, ''));
27
+ flaggedCommentsCount.textContent = `(${currentCount - 1})`;
28
+ } else {
29
+ alert("Failed to keep comment. Please try again.");
30
+ }
31
+ } catch (err) {
32
+ console.error(err);
33
+ alert("Error processing comment.");
34
+ }
35
+ });
36
+ });
static/logo.png ADDED

Git LFS Details

  • SHA256: 8f418d5d590a7a84776ea7e9321ee27fadc4a48e007c24652cfd89fcfd4dea5d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.25 MB
templates/base.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{% block title %}YouTube Comment Moderator{% endblock %}</title>
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
10
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js" defer></script>
11
+ </head>
12
+ <body class="bg-gray-100 min-h-screen flex flex-col">
13
+ <nav class="bg-blue-600 text-white p-4 shadow-md">
14
+ <div class="container mx-auto flex justify-between items-center">
15
+ <a href="/" class="text-2xl font-bold">
16
+ <i class="fas fa-shield-alt mr-2"></i>YouTube Comment Guard
17
+ </a>
18
+ {% if current_user %}
19
+ <div class="flex items-center space-x-4">
20
+ <span>Welcome, {{ current_user.username }}</span>
21
+ <a href="/logout" class="bg-red-500 hover:bg-red-600 px-3 py-2 rounded">
22
+ Logout
23
+ </a>
24
+ </div>
25
+ {% else %}
26
+ <a href="/login" class="bg-green-500 hover:bg-green-600 px-4 py-2 rounded">
27
+ Login
28
+ </a>
29
+ {% endif %}
30
+ </div>
31
+ </nav>
32
+
33
+ <main class="container mx-auto px-4 py-8 flex-grow">
34
+ {% block content %}{% endblock %}
35
+ </main>
36
+
37
+ <footer class="bg-gray-800 text-white py-4 text-center">
38
+ <p>&copy; 2024 YouTube Comment Guard. All rights reserved.</p>
39
+ </footer>
40
+
41
+ {% block scripts %}{% endblock %}
42
+ </body>
43
+ </html>
templates/index.html ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gambling Comment Filter</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --primary: #4361ee;
12
+ --secondary: #3a0ca3;
13
+ --light: #f8f9fa;
14
+ --dark: #212529;
15
+ --success: #2dc653;
16
+ --danger: #e63946;
17
+ --warning: #ff9f1c;
18
+ --info: #90e0ef;
19
+ --border-radius: 0.5rem;
20
+ --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
21
+ --transition: all 0.3s ease;
22
+ }
23
+
24
+ body {
25
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
26
+ background-color: #f5f7fa;
27
+ color: #333;
28
+ line-height: 1.6;
29
+ padding: 0;
30
+ margin: 0;
31
+ }
32
+
33
+ .navbar {
34
+ background-color: var(--primary);
35
+ box-shadow: var(--box-shadow);
36
+ padding: 1rem 0;
37
+ }
38
+
39
+ .navbar-brand {
40
+ color: white;
41
+ font-weight: 700;
42
+ font-size: 1.5rem;
43
+ padding-left: 1rem;
44
+ }
45
+
46
+ .app-container {
47
+ max-width: 1200px;
48
+ margin: 2rem auto;
49
+ padding: 0 1rem;
50
+ }
51
+
52
+ .card {
53
+ border: none;
54
+ border-radius: var(--border-radius);
55
+ box-shadow: var(--box-shadow);
56
+ margin-bottom: 2rem;
57
+ overflow: hidden;
58
+ transition: var(--transition);
59
+ }
60
+
61
+ .card:hover {
62
+ box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
63
+ }
64
+
65
+ .card-header {
66
+ background-color: white;
67
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
68
+ font-weight: 600;
69
+ padding: 1.25rem 1.5rem;
70
+ }
71
+
72
+ .card-body {
73
+ padding: 1.5rem;
74
+ }
75
+
76
+ .form-label {
77
+ font-weight: 500;
78
+ margin-bottom: 0.5rem;
79
+ color: #495057;
80
+ }
81
+
82
+ .form-control {
83
+ border-radius: var(--border-radius);
84
+ padding: 0.75rem 1rem;
85
+ border: 1px solid #ced4da;
86
+ transition: var(--transition);
87
+ }
88
+
89
+ .form-control:focus {
90
+ border-color: var(--primary);
91
+ box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.25);
92
+ }
93
+
94
+ .btn {
95
+ border-radius: var(--border-radius);
96
+ padding: 0.75rem 1.5rem;
97
+ font-weight: 500;
98
+ transition: var(--transition);
99
+ }
100
+
101
+ .btn-primary {
102
+ background-color: var(--primary);
103
+ border-color: var(--primary);
104
+ }
105
+
106
+ .btn-primary:hover {
107
+ background-color: var(--secondary);
108
+ border-color: var(--secondary);
109
+ }
110
+
111
+ .result {
112
+ background-color: #f8f9fa;
113
+ border-radius: var(--border-radius);
114
+ padding: 1.5rem;
115
+ margin-bottom: 2rem;
116
+ }
117
+
118
+ .result-header {
119
+ display: flex;
120
+ align-items: center;
121
+ margin-bottom: 1rem;
122
+ }
123
+
124
+ .result-icon {
125
+ font-size: 1.5rem;
126
+ margin-right: 0.75rem;
127
+ }
128
+
129
+ .result-title {
130
+ font-size: 1.25rem;
131
+ font-weight: 600;
132
+ margin: 0;
133
+ }
134
+
135
+ .badge {
136
+ font-size: 0.85rem;
137
+ padding: 0.5rem 0.75rem;
138
+ border-radius: 50px;
139
+ font-weight: 500;
140
+ }
141
+
142
+ .rules-container {
143
+ background-color: white;
144
+ border-radius: var(--border-radius);
145
+ box-shadow: var(--box-shadow);
146
+ padding: 1.5rem;
147
+ }
148
+
149
+ .rules-category {
150
+ margin-bottom: 2rem;
151
+ }
152
+
153
+ .rules-category h3 {
154
+ font-size: 1.1rem;
155
+ font-weight: 600;
156
+ margin-bottom: 1rem;
157
+ padding-bottom: 0.5rem;
158
+ border-bottom: 2px solid var(--primary);
159
+ color: var(--primary);
160
+ }
161
+
162
+ .rules-list {
163
+ list-style-type: none;
164
+ padding: 0;
165
+ margin: 0;
166
+ max-height: 200px;
167
+ overflow-y: auto;
168
+ }
169
+
170
+ .rules-list li {
171
+ padding: 0.5rem 0.75rem;
172
+ background-color: #f8f9fa;
173
+ margin-bottom: 0.5rem;
174
+ border-radius: var(--border-radius);
175
+ font-size: 0.9rem;
176
+ display: flex;
177
+ align-items: center;
178
+ }
179
+
180
+ .rules-list li:before {
181
+ content: "•";
182
+ color: var(--primary);
183
+ font-weight: bold;
184
+ margin-right: 0.5rem;
185
+ }
186
+
187
+ pre {
188
+ background-color: #f8f9fa;
189
+ padding: 1rem;
190
+ border-radius: var(--border-radius);
191
+ white-space: pre-wrap;
192
+ }
193
+
194
+ .tabs {
195
+ display: flex;
196
+ background-color: white;
197
+ border-radius: var(--border-radius);
198
+ box-shadow: var(--box-shadow);
199
+ margin-bottom: 2rem;
200
+ overflow: hidden;
201
+ }
202
+
203
+ .tab {
204
+ flex: 1;
205
+ text-align: center;
206
+ padding: 1rem;
207
+ cursor: pointer;
208
+ transition: var(--transition);
209
+ border-bottom: 3px solid transparent;
210
+ font-weight: 500;
211
+ }
212
+
213
+ .tab.active {
214
+ background-color: white;
215
+ color: var(--primary);
216
+ border-bottom: 3px solid var(--primary);
217
+ }
218
+
219
+ .tab-icon {
220
+ margin-right: 0.5rem;
221
+ }
222
+
223
+ .tab-content {
224
+ display: none;
225
+ }
226
+
227
+ .tab-content.active {
228
+ display: block;
229
+ }
230
+
231
+ .footer {
232
+ text-align: center;
233
+ padding: 2rem 0;
234
+ margin-top: 2rem;
235
+ background-color: white;
236
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
237
+ }
238
+ </style>
239
+ </head>
240
+ <body>
241
+ <!-- Navbar -->
242
+ <nav class="navbar navbar-dark">
243
+ <div class="container">
244
+ <span class="navbar-brand">
245
+ <i class="fas fa-shield-alt me-2"></i>
246
+ Gambling Comment Filter
247
+ </span>
248
+ </div>
249
+ </nav>
250
+
251
+ <div class="app-container">
252
+ <!-- Results Section (if available) -->
253
+ {% if result %}
254
+ <div class="card mb-4">
255
+ <div class="card-header d-flex align-items-center">
256
+ <i class="fas fa-chart-bar me-2"></i>
257
+ Analysis Results
258
+ </div>
259
+ <div class="card-body">
260
+ {% if result.message %}
261
+ <div class="alert alert-success">
262
+ <i class="fas fa-check-circle me-2"></i>
263
+ {{ result.message }}
264
+ </div>
265
+ {% elif result.upload_result %}
266
+ <div class="result">
267
+ <div class="result-header">
268
+ <div class="result-icon text-primary">
269
+ <i class="fas fa-file-alt"></i>
270
+ </div>
271
+ <h5 class="result-title">File Upload Results</h5>
272
+ </div>
273
+ <div class="row mb-3">
274
+ <div class="col-md-6">
275
+ <div class="card bg-light">
276
+ <div class="card-body text-center">
277
+ <h3 class="text-danger mb-1">{{ result.upload_result.gambling_comments | length }}</h3>
278
+ <p class="mb-0">Gambling Comments Found</p>
279
+ </div>
280
+ </div>
281
+ </div>
282
+ <div class="col-md-6">
283
+ <div class="card bg-light">
284
+ <div class="card-body text-center">
285
+ <h3 class="text-success mb-1">{{ result.upload_result.safe_comments | length }}</h3>
286
+ <p class="mb-0">Safe Comments Found</p>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ <details>
292
+ <summary class="mb-2 btn btn-sm btn-outline-secondary">View Detailed Results</summary>
293
+ <pre>{{ result.upload_result | pretty_json | safe }}</pre>
294
+ </details>
295
+ </div>
296
+ {% else %}
297
+ <div class="result">
298
+ <div class="result-header">
299
+ <div class="result-icon {% if result.is_gambling %}text-danger{% else %}text-success{% endif %}">
300
+ <i class="fas {% if result.is_gambling %}fa-exclamation-triangle{% else %}fa-check-circle{% endif %}"></i>
301
+ </div>
302
+ <h5 class="result-title">Classification Result</h5>
303
+ </div>
304
+ <div class="mb-3">
305
+ <span class="badge {% if result.is_gambling %}bg-danger{% else %}bg-success{% endif %} me-2">
306
+ <i class="fas {% if result.is_gambling %}fa-times{% else %}fa-check{% endif %} me-1"></i>
307
+ {{ "Gambling Comment" if result.is_gambling else "Safe Comment" }}
308
+ </span>
309
+ <span class="badge bg-info text-dark">
310
+ <i class="fas fa-chart-line me-1"></i>
311
+ Confidence: {{ result.metrics.confidence_score }}
312
+ </span>
313
+ </div>
314
+ <details>
315
+ <summary class="mb-2 btn btn-sm btn-outline-secondary">View Analysis Details</summary>
316
+ <pre>{{ result.upload_result | pretty_json | safe }}</pre>
317
+ </details>
318
+ </div>
319
+ {% endif %}
320
+ </div>
321
+ </div>
322
+ {% endif %}
323
+
324
+ <!-- Tabs for Different Functions -->
325
+ <div class="tabs">
326
+ <div class="tab active" data-tab="classify">
327
+ <i class="fas fa-search tab-icon"></i>
328
+ Classify Comment
329
+ </div>
330
+ <div class="tab" data-tab="upload">
331
+ <i class="fas fa-upload tab-icon"></i>
332
+ Batch Upload
333
+ </div>
334
+ <div class="tab" data-tab="rules">
335
+ <i class="fas fa-cogs tab-icon"></i>
336
+ Manage Rules
337
+ </div>
338
+ </div>
339
+
340
+ <!-- Tab Contents -->
341
+ <div class="tab-content active" id="classify-tab">
342
+ <div class="card">
343
+ <div class="card-header">
344
+ <i class="fas fa-search me-2"></i>
345
+ Classify Single Comment
346
+ </div>
347
+ <div class="card-body">
348
+ <form action="/classify" method="post">
349
+ <div class="form-group mb-3">
350
+ <label for="comment" class="form-label">Enter your comment:</label>
351
+ <textarea class="form-control" name="comment" id="comment" rows="4" placeholder="Type or paste the comment here...">{{ comment }}</textarea>
352
+ </div>
353
+ <button type="submit" class="btn btn-primary">
354
+ <i class="fas fa-check-circle me-2"></i>
355
+ Analyze Comment
356
+ </button>
357
+ </form>
358
+ </div>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="tab-content" id="upload-tab">
363
+ <div class="card">
364
+ <div class="card-header">
365
+ <i class="fas fa-upload me-2"></i>
366
+ Batch Upload &amp; Process
367
+ </div>
368
+ <div class="card-body">
369
+ <form action="/upload" method="post" enctype="multipart/form-data">
370
+ <div class="form-group mb-3">
371
+ <label for="file" class="form-label">Select File for Analysis:</label>
372
+ <input type="file" class="form-control" name="file" id="file">
373
+ <small class="form-text text-muted">Supported formats: CSV, JSON, Excel</small>
374
+ </div>
375
+ <div class="form-group mb-3">
376
+ <label for="column" class="form-label">Column Name:</label>
377
+ <input type="text" class="form-control" name="column" id="column" value="comment" placeholder="Column containing comments">
378
+ <small class="form-text text-muted">Default is "comment"</small>
379
+ </div>
380
+ <button type="submit" class="btn btn-primary">
381
+ <i class="fas fa-file-import me-2"></i>
382
+ Upload &amp; Process File
383
+ </button>
384
+ </form>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ <div class="tab-content" id="rules-tab">
390
+ <div class="card mb-4">
391
+ <div class="card-header">
392
+ <i class="fas fa-plus-circle me-2"></i>
393
+ Add New Rule
394
+ </div>
395
+ <div class="card-body">
396
+ <form action="/add_rule" method="post">
397
+ <div class="row">
398
+ <div class="col-md-6">
399
+ <div class="form-group mb-3">
400
+ <label for="rule_type" class="form-label">Rule Type:</label>
401
+ <select name="rule_type" id="rule_type" class="form-control">
402
+ <option value="platform">Platform Name</option>
403
+ <option value="gambling_term">Gambling Term</option>
404
+ <option value="safe_indicator">Safe Indicator</option>
405
+ <option value="gambling_context">Gambling Context</option>
406
+ <option value="ambiguous_term">Ambiguous Term</option>
407
+ </select>
408
+ </div>
409
+ </div>
410
+ <div class="col-md-6">
411
+ <div class="form-group mb-3">
412
+ <label for="rule_value" class="form-label">Rule Value:</label>
413
+ <input type="text" class="form-control" name="rule_value" id="rule_value" placeholder="Enter new rule value">
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <button type="submit" class="btn btn-primary">
418
+ <i class="fas fa-plus me-2"></i>
419
+ Add Rule
420
+ </button>
421
+ </form>
422
+ </div>
423
+ </div>
424
+
425
+ <div class="rules-container">
426
+ <h2 class="mb-4">Current Rules</h2>
427
+ <div class="row">
428
+ <div class="col-md-4">
429
+ <div class="rules-category">
430
+ <h3><i class="fas fa-dice me-2"></i>Platform Names</h3>
431
+ <ul class="rules-list">
432
+ {% for rule in rules.platform %}
433
+ <li>{{ rule }}</li>
434
+ {% endfor %}
435
+ </ul>
436
+ </div>
437
+ </div>
438
+ <div class="col-md-4">
439
+ <div class="rules-category">
440
+ <h3><i class="fas fa-coins me-2"></i>Gambling Terms</h3>
441
+ <ul class="rules-list">
442
+ {% for rule in rules.gambling_term %}
443
+ <li>{{ rule }}</li>
444
+ {% endfor %}
445
+ </ul>
446
+ </div>
447
+ </div>
448
+ <div class="col-md-4">
449
+ <div class="rules-category">
450
+ <h3><i class="fas fa-shield-alt me-2"></i>Safe Indicators</h3>
451
+ <ul class="rules-list">
452
+ {% for rule in rules.safe_indicator %}
453
+ <li>{{ rule }}</li>
454
+ {% endfor %}
455
+ </ul>
456
+ </div>
457
+ </div>
458
+ </div>
459
+ <div class="row">
460
+ <div class="col-md-6">
461
+ <div class="rules-category">
462
+ <h3><i class="fas fa-comment-alt me-2"></i>Gambling Contexts</h3>
463
+ <ul class="rules-list">
464
+ {% for rule in rules.gambling_context %}
465
+ <li>{{ rule }}</li>
466
+ {% endfor %}
467
+ </ul>
468
+ </div>
469
+ </div>
470
+ <div class="col-md-6">
471
+ <div class="rules-category">
472
+ <h3><i class="fas fa-question-circle me-2"></i>Ambiguous Terms</h3>
473
+ <ul class="rules-list">
474
+ {% for rule in rules.ambiguous_term %}
475
+ <li>{{ rule }}</li>
476
+ {% endfor %}
477
+ </ul>
478
+ </div>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ <footer class="footer">
486
+ <div class="container">
487
+ <p class="mb-0">Gambling Comment Filter &copy; 2025 | All Rights Reserved</p>
488
+ </div>
489
+ </footer>
490
+
491
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
492
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
493
+ <script>
494
+ // Tab functionality
495
+ document.addEventListener('DOMContentLoaded', function() {
496
+ const tabs = document.querySelectorAll('.tab');
497
+
498
+ tabs.forEach(tab => {
499
+ tab.addEventListener('click', function() {
500
+ // Remove active class from all tabs
501
+ tabs.forEach(t => t.classList.remove('active'));
502
+
503
+ // Add active class to clicked tab
504
+ this.classList.add('active');
505
+
506
+ // Hide all tab contents
507
+ document.querySelectorAll('.tab-content').forEach(content => {
508
+ content.classList.remove('active');
509
+ });
510
+
511
+ // Show the corresponding tab content
512
+ const tabId = this.getAttribute('data-tab') + '-tab';
513
+ document.getElementById(tabId).classList.add('active');
514
+ });
515
+ });
516
+ });
517
+ </script>
518
+ </body>
519
+ </html>
templates/login.html ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>YouTube Comment Moderator</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ background-color: #f4f4f4;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ min-height: 100vh;
15
+ font-family: 'Arial', sans-serif;
16
+ }
17
+ .login-container {
18
+ max-width: 400px;
19
+ width: 100%;
20
+ padding: 2rem;
21
+ background-color: white;
22
+ border-radius: 12px;
23
+ box-shadow: 0 10px 25px rgba(0,0,0,0.1);
24
+ }
25
+ .login-header {
26
+ text-align: center;
27
+ margin-bottom: 2rem;
28
+ }
29
+ .login-header img {
30
+ width: 80px;
31
+ margin-bottom: 1rem;
32
+ }
33
+ .btn-youtube {
34
+ background-color: #FF0000;
35
+ color: white;
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ gap: 10px;
40
+ transition: background-color 0.3s ease;
41
+ }
42
+ .btn-youtube:hover {
43
+ background-color: #CC0000;
44
+ color: white;
45
+ }
46
+ .login-footer {
47
+ text-align: center;
48
+ margin-top: 1rem;
49
+ color: #6c757d;
50
+ font-size: 0.8rem;
51
+ }
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <div class="login-container">
56
+ <div class="login-header">
57
+ <img src="../static/logo.png" alt="YouTube Comment Moderator">
58
+ <h2>YouTube Comment Moderator</h2>
59
+ <p class="text-muted">Simplify your comment management</p>
60
+ </div>
61
+
62
+ {% if error %}
63
+ <div class="alert alert-danger mb-3">{{ error }}</div>
64
+ {% endif %}
65
+
66
+ <form action="/login" method="post">
67
+ <button type="submit" class="btn btn-youtube w-100 py-2">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="white">
69
+ <path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.246 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/>
70
+ </svg>
71
+ Login with YouTube
72
+ </button>
73
+ </form>
74
+
75
+ <div class="login-footer">
76
+ <p>Securely powered by Google OAuth</p>
77
+ </div>
78
+ </div>
79
+
80
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
81
+ </body>
82
+ </html>
templates/video_comments.html ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}
4
+ Moderate Comments - {{ video.title }}
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+ <div class="container mx-auto px-4 py-8">
9
+ <!-- Outer container with card styling -->
10
+ <div class="bg-white shadow-xl rounded-lg overflow-visible">
11
+ <!-- Header section with video title and controls -->
12
+ <div class="bg-gradient-to-r from-blue-500 to-purple-600 p-6">
13
+ <div class="flex justify-between items-center">
14
+ <h1 class="text-3xl font-bold text-white flex items-center">
15
+ <i class="fab fa-youtube mr-4 text-red-500"></i>
16
+ {{ video.title }}
17
+ </h1>
18
+ <div class="flex space-x-4">
19
+ <button id="refreshCommentsBtn" class="bg-white text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition duration-300 flex items-center shadow-md">
20
+ <i class="fas fa-sync mr-2"></i>Refresh Comments
21
+ </button>
22
+ <div x-data="{ showSettings: false, saveSettings() {
23
+ // Add your save logic here, e.g., update a model or call an API
24
+ alert('Settings saved!');
25
+ this.showSettings = false; // Optionally hide the settings box after saving
26
+ } }" class="relative">
27
+ <button @click="showSettings = !showSettings" class="bg-white text-purple-600 px-4 py-2 rounded-lg hover:bg-purple-50 transition duration-300 flex items-center shadow-md">
28
+ <i class="fas fa-cog mr-2"></i>Moderation Settings
29
+ </button>
30
+ <div x-show="showSettings" x-transition class="absolute right-0 mt-2 w-72 bg-white rounded-lg shadow-2xl p-6 z-20 border border-gray-100">
31
+ <h3 class="text-xl font-semibold mb-4 text-gray-800">Moderation Settings</h3>
32
+ <div class="space-y-4">
33
+ <div class="flex items-center">
34
+ <input type="checkbox" id="auto-delete" class="mr-3 rounded text-blue-500 focus:ring-blue-400">
35
+ <label for="auto-delete" class="text-gray-700">Auto-delete flagged comments</label>
36
+ </div>
37
+ <div>
38
+ <label class="block mb-2 text-gray-700">Gambling Confidence Threshold</label>
39
+ <input type="range" min="0" max="1" step="0.05" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" x-model="threshold" value="0.55">
40
+ </div>
41
+ <button @click="saveSettings" class="w-full bg-gradient-to-r from-blue-500 to-purple-600 text-white py-2 rounded-lg hover:opacity-90 transition duration-300">
42
+ Save Settings
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </div>
47
+
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Comments Section -->
53
+ <div class="grid md:grid-cols-2 gap-6 p-6">
54
+ <!-- Safe Comments Column -->
55
+ <div>
56
+ <h2 class="text-2xl font-semibold mb-4 flex items-center text-green-600">
57
+ <i class="fas fa-check-circle mr-3"></i>Safe Comments
58
+ <span class="text-sm text-gray-500 ml-2">({{ safe_comments|length }})</span>
59
+ </h2>
60
+ <div class="space-y-4">
61
+ {% for comment in safe_comments %}
62
+ <div class="bg-white p-4 rounded-lg shadow-md border border-gray-100">
63
+ <p class="mb-2 text-gray-800">{{ comment.text }}</p>
64
+ <div class="text-sm text-gray-600 flex justify-between items-center">
65
+ <span class="font-medium">{{ comment.author }}</span>
66
+ <span class="text-xs text-green-600">Safe</span>
67
+ </div>
68
+ </div>
69
+ {% endfor %}
70
+ </div>
71
+ </div>
72
+ <!-- Flagged Comments Column -->
73
+ <div>
74
+ <h2 class="text-2xl font-semibold mb-4 flex items-center text-red-600">
75
+ <i class="fas fa-exclamation-triangle mr-3"></i>Flagged Comments
76
+ <span id="flaggedCount" class="text-sm text-gray-500 ml-2">({{ flagged_comments|length }})</span>
77
+
78
+ </h2>
79
+ <div class="space-y-4">
80
+ {% for comment in flagged_comments %}
81
+ <div class="comment-card bg-red-50 p-4 rounded-lg shadow-md border border-red-200 relative">
82
+ <p class="mb-2 text-gray-800">{{ comment.text }}</p>
83
+ <div class="text-sm text-gray-600 flex justify-between items-center">
84
+ <span class="font-medium">{{ comment.author }}</span>
85
+ <div class="flex space-x-2">
86
+ <button data-comment-id="{{ comment.id }}" data-video-id="{{ video.id }}" class="delete-comment-btn bg-red-500 text-white px-3 py-1 rounded hover:bg-red-600 transition duration-300">
87
+ Delete
88
+ </button>
89
+ <button data-comment-id="{{ comment.id }}" data-video-id="{{ video.id }}"class="keep-comment-btn bg-green-500 text-white px-3 py-1 rounded hover:bg-green-600 transition duration-300">
90
+ Keep
91
+ </button>
92
+ </div>
93
+ </div>
94
+ <div class="mt-2 text-xs text-gray-500">
95
+ <strong>Gambling Confidence:</strong> {{ comment.metrics.confidence_score }}
96
+ </div>
97
+ </div>
98
+ {% endfor %}
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ {% endblock %}
105
+
106
+ {% block scripts %}
107
+ <script>
108
+ document.addEventListener('DOMContentLoaded', function() {
109
+ // Delete Comment Functionality
110
+ document.querySelectorAll('.delete-comment-btn').forEach(button => {
111
+ button.addEventListener('click', async function(e) {
112
+ e.stopPropagation(); // Ensure this click doesn't affect other handlers
113
+ const commentId = this.getAttribute('data-comment-id');
114
+ const videoId = this.getAttribute('data-video-id');
115
+ const commentCard = this.closest('.comment-card');
116
+
117
+ try {
118
+ const response = await fetch(`/api/comments/${commentId}?video_id=${videoId}`, {
119
+ method: 'DELETE'
120
+ });
121
+ const data = await response.json();
122
+
123
+ if (data.success) {
124
+ // Remove the comment from the DOM
125
+ commentCard.remove();
126
+
127
+ // Optionally, update the flagged comments count
128
+ const flaggedCommentsCount = document.querySelector('h2 span');
129
+ const currentCount = parseInt(flaggedCommentsCount.textContent.replace(/[()]/g, ''));
130
+ flaggedCommentsCount.textContent = `(${currentCount - 1})`;
131
+ } else {
132
+ alert("Failed to delete comment. Please refresh and try again.");
133
+ }
134
+ } catch (err) {
135
+ console.error(err);
136
+ alert("Error deleting comment.");
137
+ }
138
+ });
139
+ });
140
+
141
+ // Keep Comment Functionality with added debug logging
142
+ document.querySelectorAll('.keep-comment-btn').forEach(button => {
143
+ button.addEventListener('click', async function(e) {
144
+ e.stopPropagation();
145
+ const commentId = this.getAttribute('data-comment-id');
146
+ const videoId = this.getAttribute('data-video-id');
147
+ const commentCard = this.closest('.comment-card');
148
+ console.log(`Keep button clicked for commentId: ${commentId}, videoId: ${videoId}`);
149
+
150
+ try {
151
+ // Make the API call to keep the comment on YouTube
152
+ const response = await fetch(`/api/comments/keep/${commentId}?video_id=${videoId}`, {
153
+ method: 'POST'
154
+ });
155
+ console.log("Response received from API:", response);
156
+ const data = await response.json();
157
+ console.log("Parsed response data:", data);
158
+
159
+ if (data.success) {
160
+ // If successful, remove from DOM and update the flagged count
161
+ commentCard.remove();
162
+ const flaggedCommentsCount = document.querySelector('#flaggedCount');
163
+ const currentCount = parseInt(flaggedCommentsCount.textContent);
164
+ flaggedCommentsCount.textContent = currentCount - 1;
165
+ console.log(`Comment ${commentId} removed, flagged count updated to ${currentCount - 1}`);
166
+ } else {
167
+ console.error(`API reported error for comment ${commentId}:`, data.error);
168
+ alert("Failed to keep comment: " + (data.error || 'Unknown error'));
169
+ }
170
+ } catch (err) {
171
+ console.error(`Error keeping comment ${commentId}:`, err);
172
+ alert("Error keeping comment.");
173
+ }
174
+ });
175
+ });
176
+
177
+
178
+ // Refresh Comments Functionality
179
+ document.getElementById('refreshCommentsBtn').addEventListener('click', function(e) {
180
+ e.stopPropagation(); // Prevent any unwanted event bubbling
181
+ const videoId = "{{ video.id }}";
182
+ window.location.href = `/video/${videoId}`;
183
+ });
184
+ });
185
+ </script>
186
+ {% endblock %}
templates/videos.html ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Your YouTube Videos{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="container mx-auto px-4 py-8">
7
+ <div class="flex items-center justify-between mb-8">
8
+ <h1 class="text-4xl font-extrabold text-gray-800 flex items-center">
9
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 mr-4 text-red-600" viewBox="0 0 20 20" fill="currentColor">
10
+ <path fill-rule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5zm12 1V5H5v1h10zm-5 8.5a3.5 3.5 0 100-7 3.5 3.5 0 000 7z" clip-rule="evenodd" />
11
+ </svg>
12
+ YouTube Video Management
13
+ </h1>
14
+ <button class="bg-green-500 text-white px-5 py-2 rounded-lg shadow-md hover:bg-green-600 transition duration-300 flex items-center">
15
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
16
+ <path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
17
+ </svg>
18
+ Refresh Videos
19
+ </button>
20
+ </div>
21
+
22
+ {% if videos %}
23
+ <div class="grid md:grid-cols-3 gap-6">
24
+ {% for video in videos %}
25
+ <div class="bg-white rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
26
+ <div class="relative">
27
+ <img src="{{ video.thumbnail }}" alt="{{ video.title }}" class="w-full h-56 object-cover rounded-t-xl">
28
+ <div class="absolute top-2 right-2 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
29
+ {{ video.duration }}
30
+ </div>
31
+ </div>
32
+ <div class="p-5">
33
+ <h3 class="font-bold text-xl mb-3 text-gray-800 line-clamp-2">{{ video.title }}</h3>
34
+ <div class="flex justify-between items-center">
35
+ <div class="flex items-center space-x-3">
36
+ <span class="text-sm text-gray-600 flex items-center">
37
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
38
+ <path d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.5 1a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-2zm5 0a.5.5 0 00-.5.5v1a.5.5 0 00.5.5h2a.5.5 0 00.5-.5v-1a.5.5 0 00-.5-.5h-2zM6 9.5a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-1zm5 0a.5.5 0 01.5-.5h2a.5.5 0 01.5.5v1a.5.5 0 01-.5.5h-2a.5.5 0 01-.5-.5v-1z" />
39
+ </svg>
40
+ {{ video.comments_count }} Comments
41
+ </span>
42
+ </div>
43
+ <a href="/video/{{ video.id }}" class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition duration-300 shadow-md">
44
+ Moderate
45
+ </a>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ {% endfor %}
50
+ </div>
51
+ {% else %}
52
+ <div class="bg-white rounded-xl shadow-lg p-12 text-center">
53
+ <div class="flex justify-center mb-6">
54
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-24 w-24 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
56
+ </svg>
57
+ </div>
58
+ <h2 class="text-2xl font-semibold text-gray-700 mb-4">No Videos Found</h2>
59
+ <p class="text-gray-500 mb-6">It looks like there are no videos to moderate at the moment.</p>
60
+ <button class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 transition duration-300 shadow-md">
61
+ Refresh Channel
62
+ </button>
63
+ </div>
64
+ {% endif %}
65
+ </div>
66
+ {% endblock %}