Ptul2x5 commited on
Commit
9cc30a5
·
verified ·
1 Parent(s): 87183f9

Update database

Browse files
app.py CHANGED
@@ -3,6 +3,7 @@ import torch
3
  from transformers import AutoTokenizer
4
  from flask import Flask, request, jsonify, render_template, redirect, url_for, flash
5
  from flask_login import LoginManager, login_user, logout_user, login_required, current_user
 
6
  from models import db, User, Feedback
7
  from forms import RegistrationForm, LoginForm
8
  from PhoBERTMultiTask import PhoBERTMultiTask
@@ -28,6 +29,18 @@ login_manager.login_message_category = 'info'
28
  def load_user(user_id):
29
  return User.query.get(int(user_id))
30
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
32
 
33
  # === Load tokenizer & model ===
@@ -98,6 +111,105 @@ def logout():
98
  def health():
99
  return jsonify({"status": "healthy", "message": "✅ PhoBERT MultiTask API is running!"})
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  @app.route("/api/feedback-history", methods=["GET"])
102
  @login_required
103
  def get_feedback_history():
@@ -199,6 +311,39 @@ def predict():
199
  # Khởi tạo database
200
  with app.app_context():
201
  db.create_all()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  print("✅ Database đã sẵn sàng!")
203
 
204
  if __name__ == "__main__":
 
3
  from transformers import AutoTokenizer
4
  from flask import Flask, request, jsonify, render_template, redirect, url_for, flash
5
  from flask_login import LoginManager, login_user, logout_user, login_required, current_user
6
+ from functools import wraps
7
  from models import db, User, Feedback
8
  from forms import RegistrationForm, LoginForm
9
  from PhoBERTMultiTask import PhoBERTMultiTask
 
29
  def load_user(user_id):
30
  return User.query.get(int(user_id))
31
 
32
+ # Decorator để yêu cầu quyền admin
33
+ def admin_required(f):
34
+ @wraps(f)
35
+ def decorated_function(*args, **kwargs):
36
+ if not current_user.is_authenticated:
37
+ return redirect(url_for('login', next=request.url))
38
+ if not current_user.is_admin:
39
+ flash('Bạn không có quyền truy cập trang này. Chỉ admin mới được phép.', 'danger')
40
+ return redirect(url_for('home'))
41
+ return f(*args, **kwargs)
42
+ return decorated_function
43
+
44
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
45
 
46
  # === Load tokenizer & model ===
 
111
  def health():
112
  return jsonify({"status": "healthy", "message": "✅ PhoBERT MultiTask API is running!"})
113
 
114
+ @app.route("/my-statistics")
115
+ @login_required
116
+ def my_statistics():
117
+ """Trang thống kê feedback cá nhân của user"""
118
+ try:
119
+ # Lấy thống kê feedback của user hiện tại
120
+ user_feedbacks = Feedback.query.filter_by(user_id=current_user.id).all()
121
+ total_feedbacks = len(user_feedbacks)
122
+
123
+ # Thống kê sentiment
124
+ sentiment_stats = db.session.query(
125
+ Feedback.sentiment,
126
+ db.func.count(Feedback.id).label('count')
127
+ ).filter_by(user_id=current_user.id).group_by(Feedback.sentiment).all()
128
+ sentiment_stats = [{'sentiment': item.sentiment, 'count': item.count} for item in sentiment_stats]
129
+
130
+ # Thống kê topic
131
+ topic_stats = db.session.query(
132
+ Feedback.topic,
133
+ db.func.count(Feedback.id).label('count')
134
+ ).filter_by(user_id=current_user.id).group_by(Feedback.topic).all()
135
+ topic_stats = [{'topic': item.topic, 'count': item.count} for item in topic_stats]
136
+
137
+ # Thống kê theo ngày (30 ngày gần nhất)
138
+ from datetime import datetime, timedelta
139
+ thirty_days_ago = datetime.now() - timedelta(days=30)
140
+ daily_stats = db.session.query(
141
+ db.func.date(Feedback.created_at).label('date'),
142
+ db.func.count(Feedback.id).label('count')
143
+ ).filter(
144
+ Feedback.user_id == current_user.id,
145
+ Feedback.created_at >= thirty_days_ago
146
+ ).group_by(
147
+ db.func.date(Feedback.created_at)
148
+ ).order_by('date').all()
149
+ daily_stats = [{'date': str(item.date), 'count': item.count} for item in daily_stats]
150
+
151
+ # Feedback gần nhất của user
152
+ recent_feedbacks = Feedback.query.filter_by(user_id=current_user.id)\
153
+ .order_by(Feedback.created_at.desc()).limit(10).all()
154
+
155
+ return render_template('my_statistics.html',
156
+ total_feedbacks=total_feedbacks,
157
+ recent_feedbacks=recent_feedbacks,
158
+ sentiment_stats=sentiment_stats,
159
+ topic_stats=topic_stats,
160
+ daily_stats=daily_stats)
161
+ except Exception as e:
162
+ flash(f'Lỗi khi tải dữ liệu: {str(e)}', 'danger')
163
+ return redirect(url_for('home'))
164
+
165
+ @app.route("/admin/database")
166
+ @admin_required
167
+ def view_database():
168
+ """Trang xem database với giao diện đẹp"""
169
+ try:
170
+ # Lấy thống kê tổng quan
171
+ total_users = User.query.count()
172
+ total_feedbacks = Feedback.query.count()
173
+
174
+ # Lấy feedbacks gần nhất
175
+ recent_feedbacks = Feedback.query.order_by(Feedback.created_at.desc()).limit(10).all()
176
+
177
+ # Thống kê sentiment
178
+ sentiment_stats = db.session.query(
179
+ Feedback.sentiment,
180
+ db.func.count(Feedback.id).label('count')
181
+ ).group_by(Feedback.sentiment).all()
182
+ sentiment_stats = [{'sentiment': item.sentiment, 'count': item.count} for item in sentiment_stats]
183
+
184
+ # Thống kê topic
185
+ topic_stats = db.session.query(
186
+ Feedback.topic,
187
+ db.func.count(Feedback.id).label('count')
188
+ ).group_by(Feedback.topic).all()
189
+ topic_stats = [{'topic': item.topic, 'count': item.count} for item in topic_stats]
190
+
191
+ # Thống kê theo ngày (7 ngày gần nhất)
192
+ from datetime import datetime, timedelta
193
+ seven_days_ago = datetime.now() - timedelta(days=7)
194
+ daily_stats = db.session.query(
195
+ db.func.date(Feedback.created_at).label('date'),
196
+ db.func.count(Feedback.id).label('count')
197
+ ).filter(Feedback.created_at >= seven_days_ago).group_by(
198
+ db.func.date(Feedback.created_at)
199
+ ).order_by('date').all()
200
+ daily_stats = [{'date': str(item.date), 'count': item.count} for item in daily_stats]
201
+
202
+ return render_template('database_view.html',
203
+ total_users=total_users,
204
+ total_feedbacks=total_feedbacks,
205
+ recent_feedbacks=recent_feedbacks,
206
+ sentiment_stats=sentiment_stats,
207
+ topic_stats=topic_stats,
208
+ daily_stats=daily_stats)
209
+ except Exception as e:
210
+ flash(f'Lỗi khi tải dữ liệu: {str(e)}', 'danger')
211
+ return redirect(url_for('home'))
212
+
213
  @app.route("/api/feedback-history", methods=["GET"])
214
  @login_required
215
  def get_feedback_history():
 
311
  # Khởi tạo database
312
  with app.app_context():
313
  db.create_all()
314
+
315
+ # Kiểm tra và thêm cột is_admin nếu chưa có
316
+ try:
317
+ # Thử query để kiểm tra xem cột is_admin có tồn tại không
318
+ db.session.execute(db.text("SELECT is_admin FROM users LIMIT 1"))
319
+ except Exception:
320
+ # Nếu cột không tồn tại, thêm cột mới
321
+ print("🔄 Đang thêm cột is_admin vào bảng users...")
322
+ try:
323
+ db.session.execute(db.text("ALTER TABLE users ADD COLUMN is_admin BOOLEAN DEFAULT 0"))
324
+ db.session.commit()
325
+ print("✅ Đã thêm cột is_admin vào database")
326
+ except Exception as e:
327
+ print(f"⚠️ Lỗi khi thêm cột: {e}")
328
+
329
+ # Tạo tài khoản admin mặc định nếu chưa có
330
+ try:
331
+ admin_user = User.query.filter_by(username='admin').first()
332
+ if not admin_user:
333
+ admin_user = User(username='admin', is_admin=True)
334
+ admin_user.set_password('123456')
335
+ db.session.add(admin_user)
336
+ db.session.commit()
337
+ print("✅ Đã tạo tài khoản admin mặc định (username: admin, password: 123456)")
338
+ else:
339
+ # Cập nhật user admin hiện có thành admin nếu chưa
340
+ if not admin_user.is_admin:
341
+ admin_user.is_admin = True
342
+ db.session.commit()
343
+ print("✅ Đã cập nhật tài khoản admin hiện có")
344
+ except Exception as e:
345
+ print(f"⚠️ Lỗi khi tạo/cập nhật admin: {e}")
346
+
347
  print("✅ Database đã sẵn sàng!")
348
 
349
  if __name__ == "__main__":
models.py CHANGED
@@ -11,6 +11,7 @@ class User(UserMixin, db.Model):
11
  id = db.Column(db.Integer, primary_key=True)
12
  username = db.Column(db.String(80), unique=True, nullable=False)
13
  password_hash = db.Column(db.String(128), nullable=False)
 
14
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
15
 
16
  # Relationship với feedbacks
 
11
  id = db.Column(db.Integer, primary_key=True)
12
  username = db.Column(db.String(80), unique=True, nullable=False)
13
  password_hash = db.Column(db.String(128), nullable=False)
14
+ is_admin = db.Column(db.Boolean, default=False, nullable=False)
15
  created_at = db.Column(db.DateTime, default=datetime.utcnow)
16
 
17
  # Relationship với feedbacks
static/.DS_Store ADDED
Binary file (6.15 kB). View file
 
static/css/style.css CHANGED
@@ -1,24 +1,22 @@
1
  :root {
2
- /* Colors */
3
- --primary-color: #374151;
4
- --primary-dark: #1F2937;
5
  --success-color: #10B981;
6
  --warning-color: #F59E0B;
7
  --danger-color: #EF4444;
8
  --white: #FFFFFF;
9
- --black: #000000;
10
-
11
  /* Grayscale System */
12
  --gray-50: #F9FAFB;
13
  --gray-100: #F3F4F6;
14
  --gray-200: #E5E7EB;
15
  --gray-300: #D1D5DB;
16
- --gray-400: #9CA3AF;
17
- --gray-500: #6B7280;
18
- --gray-600: #4B5563;
19
- --gray-700: #374151;
20
- --gray-800: #1F2937;
21
- --gray-900: #111827;
22
 
23
  /* Layout */
24
  --bg-primary: #FFFFFF;
@@ -30,9 +28,11 @@
30
  --border-color: #E5E7EB;
31
  --shadow-color: rgba(0, 0, 0, 0.1);
32
 
33
- /* Bootstrap overrides */
34
- --bs-primary: #374151 !important;
35
- --bs-primary-rgb: 55, 65, 81 !important;
 
 
36
  }
37
 
38
  /* Base Styles */
@@ -58,17 +58,22 @@ body {
58
  background: transparent;
59
  border-radius: 24px;
60
  margin: 0 20px 20px 0;
61
- padding: 0 !important;
62
  position: relative;
63
  overflow: hidden;
64
- margin-left: 0 !important;
65
- padding-left: 0 !important;
 
 
 
 
 
66
  }
67
 
68
  /* Header */
69
  header {
70
- background: var(--gray-400) !important;
71
- color: #FFFFFF !important;
72
  border-radius: 16px;
73
  margin: 0 0 1.5rem 0;
74
  padding: 1.5rem 2rem;
@@ -76,36 +81,20 @@ header {
76
  overflow: hidden;
77
  }
78
 
79
- /* Force header color consistency */
80
- body header,
81
- body header *,
82
- header.bg-primary,
83
- header.text-primary,
84
- header .bg-primary,
85
- header .text-primary {
86
- background: var(--gray-400) !important;
87
- background-image: none !important;
88
- margin-left: 0 !important;
89
- padding-left: 0 !important;
90
- }
91
-
92
  header h1,
93
  header .display-4,
94
- header h1.display-4,
95
  header .text-primary {
96
- color: #FFFFFF !important;
97
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 8px rgba(0, 0, 0, 0.2);
98
  margin: 0;
99
  font-weight: 700;
100
  letter-spacing: -0.025em;
101
  font-size: 2.2rem;
102
  position: relative;
103
  z-index: 1;
104
- background: none;
105
- padding: 0;
106
  }
107
 
108
- /* Hide dark mode elements */
109
  header button,
110
  header .btn,
111
  header [class*="toggle"],
@@ -116,13 +105,12 @@ header [id*="theme"],
116
  .fa-sun,
117
  [class*="dark-mode"],
118
  [class*="theme-toggle"] {
119
- display: none !important;
120
- visibility: hidden !important;
121
  }
122
 
123
  header p,
124
- header .lead,
125
- header p.lead {
126
  display: none;
127
  }
128
 
@@ -239,19 +227,12 @@ header p.lead {
239
  font-size: 1rem;
240
  }
241
 
242
- .btn-primary,
243
- .btn.btn-primary,
244
- .btn-primary:visited,
245
- .btn-primary:link,
246
- button.btn-primary,
247
- button.btn.btn-primary,
248
- #analyzeBtn.btn-primary,
249
- #analyzeBtn.btn.btn-primary {
250
- background: var(--gray-400) !important;
251
- border: 1px solid var(--gray-400) !important;
252
- color: var(--white) !important;
253
  font-weight: 600;
254
- box-shadow: 0 4px 12px rgba(168, 200, 236, 0.3);
255
  position: relative;
256
  overflow: hidden;
257
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -274,15 +255,20 @@ button.btn.btn-primary,
274
  .btn-primary:active,
275
  .btn-primary:not(:disabled):not(.disabled):active,
276
  .btn-primary:not(:disabled):not(.disabled).active,
277
- .show > .btn-primary.dropdown-toggle,
278
- button.btn-primary:hover,
279
- button.btn.btn-primary:hover,
280
- #analyzeBtn.btn-primary:hover,
281
- #analyzeBtn.btn.btn-primary:hover {
 
 
 
 
 
 
282
  background: var(--gray-500) !important;
283
  border-color: var(--gray-500) !important;
284
  color: var(--white) !important;
285
- box-shadow: 0 0 0 3px rgba(55, 65, 81, 0.25) !important;
286
  }
287
 
288
  .btn-outline-secondary {
@@ -338,26 +324,32 @@ button.btn.btn-primary:hover,
338
  .topic-facility { color: var(--gray-600) !important; font-weight: 600; }
339
  .topic-others { color: var(--gray-500) !important; font-weight: 600; }
340
 
341
- /* All Cards - Unified Light Gray Color Scheme */
342
- .card-header,
343
- .card-header.bg-success,
344
- .card-header.bg-primary,
345
- .card-header.bg-info,
346
- .card-header.bg-light,
347
- body .card-header,
348
- body .card-header.bg-success,
349
- body .card-header.bg-primary,
350
- body .card-header.bg-info,
351
- body .card-header.bg-light {
352
- background: var(--gray-400) !important;
353
- color: #FFFFFF !important;
354
  border-bottom: 1px solid var(--gray-300);
355
  }
356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  /* Button Container Spacing */
358
  .d-flex.gap-2 {
359
- gap: 0.5rem !important;
360
- margin-top: 0.75rem !important;
361
  }
362
 
363
  .d-flex.gap-2 .btn {
@@ -419,11 +411,19 @@ header .btn-light:hover {
419
  }
420
 
421
  .alert {
422
- border-radius: 8px;
423
  border: 1px solid var(--gray-200);
424
- padding: 1rem 1.5rem;
425
  background: var(--white);
426
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
 
 
 
 
 
 
 
 
427
  }
428
 
429
  .alert.fade-out {
@@ -431,6 +431,86 @@ header .btn-light:hover {
431
  transform: translateY(-10px);
432
  }
433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  blockquote {
435
  border-left: 4px solid var(--primary-color);
436
  padding-left: 1.5rem;
@@ -450,18 +530,70 @@ footer {
450
  }
451
 
452
  /* Bootstrap Overrides */
453
- .bg-primary,
454
- .card-header.bg-primary {
455
- background: var(--gray-400) !important;
 
 
 
456
  }
457
 
458
- .text-primary,
459
  .spinner-border.text-primary {
460
- color: var(--primary-color) !important;
461
  }
462
 
463
  .border-primary {
464
- border-color: var(--primary-color) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  }
466
 
467
  /* Animations */
@@ -533,19 +665,106 @@ footer {
533
  background: rgba(135, 206, 235, 0.8);
534
  }
535
 
536
- /* Responsive */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  @media (max-width: 768px) {
538
- .container-fluid { margin: 0 10px 10px 0; }
 
 
539
 
540
  header {
541
  border-radius: 12px;
542
- padding: 1rem 1rem !important;
543
  margin: 0 0 1rem 0;
544
  }
545
 
546
- header h1 { font-size: 1.8rem; }
 
 
547
 
548
- .card-body { padding: 1.25rem; }
 
 
549
 
550
  .btn {
551
  padding: 0.75rem 1.5rem;
@@ -553,7 +772,7 @@ footer {
553
  }
554
 
555
  .card-header {
556
- border-radius: 12px 12px 0 0 !important;
557
  padding: 1rem;
558
  }
559
 
@@ -566,4 +785,47 @@ footer {
566
  padding: 0.4rem 0.8rem;
567
  font-size: 0.85rem;
568
  }
569
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  :root {
2
+ /* Color System */
3
+ --primary-color: #6B7280;
 
4
  --success-color: #10B981;
5
  --warning-color: #F59E0B;
6
  --danger-color: #EF4444;
7
  --white: #FFFFFF;
8
+
 
9
  /* Grayscale System */
10
  --gray-50: #F9FAFB;
11
  --gray-100: #F3F4F6;
12
  --gray-200: #E5E7EB;
13
  --gray-300: #D1D5DB;
14
+ --gray-400: #6B7280;
15
+ --gray-500: #4B5563;
16
+ --gray-600: #374151;
17
+ --gray-700: #1F2937;
18
+ --gray-800: #111827;
19
+ --gray-900: #000000;
20
 
21
  /* Layout */
22
  --bg-primary: #FFFFFF;
 
28
  --border-color: #E5E7EB;
29
  --shadow-color: rgba(0, 0, 0, 0.1);
30
 
31
+ /* Glass button styles */
32
+ --glass-bg: rgba(255,255,255,0.2);
33
+ --glass-border: rgba(255,255,255,0.3);
34
+ --glass-hover-bg: rgba(255,255,255,0.3);
35
+ --glass-shadow: 0 4px 8px rgba(0,0,0,0.2);
36
  }
37
 
38
  /* Base Styles */
 
58
  background: transparent;
59
  border-radius: 24px;
60
  margin: 0 20px 20px 0;
61
+ padding: 0;
62
  position: relative;
63
  overflow: hidden;
64
+ }
65
+
66
+ /* Full height container for login/register pages */
67
+ .container-fluid.min-vh-100 {
68
+ min-height: 100vh;
69
+ margin: 0;
70
+ border-radius: 0;
71
  }
72
 
73
  /* Header */
74
  header {
75
+ background: var(--gray-400);
76
+ color: #FFFFFF;
77
  border-radius: 16px;
78
  margin: 0 0 1.5rem 0;
79
  padding: 1.5rem 2rem;
 
81
  overflow: hidden;
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  header h1,
85
  header .display-4,
 
86
  header .text-primary {
87
+ color: #F3F4F6 !important;
88
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
89
  margin: 0;
90
  font-weight: 700;
91
  letter-spacing: -0.025em;
92
  font-size: 2.2rem;
93
  position: relative;
94
  z-index: 1;
 
 
95
  }
96
 
97
+ /* Hide unused elements */
98
  header button,
99
  header .btn,
100
  header [class*="toggle"],
 
105
  .fa-sun,
106
  [class*="dark-mode"],
107
  [class*="theme-toggle"] {
108
+ display: none;
109
+ visibility: hidden;
110
  }
111
 
112
  header p,
113
+ header .lead {
 
114
  display: none;
115
  }
116
 
 
227
  font-size: 1rem;
228
  }
229
 
230
+ .btn-primary {
231
+ background: var(--gray-400);
232
+ border: 1px solid var(--gray-400);
233
+ color: var(--white);
 
 
 
 
 
 
 
234
  font-weight: 600;
235
+ box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3);
236
  position: relative;
237
  overflow: hidden;
238
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
255
  .btn-primary:active,
256
  .btn-primary:not(:disabled):not(.disabled):active,
257
  .btn-primary:not(:disabled):not(.disabled).active,
258
+ .show > .btn-primary.dropdown-toggle {
259
+ background: var(--gray-500) !important;
260
+ border-color: var(--gray-500) !important;
261
+ color: var(--white) !important;
262
+ box-shadow: 0 0 0 3px rgba(75, 85, 99, 0.25) !important;
263
+ }
264
+
265
+ /* Specific override for analyze button */
266
+ #analyzeBtn:hover,
267
+ #analyzeBtn:focus,
268
+ #analyzeBtn:active {
269
  background: var(--gray-500) !important;
270
  border-color: var(--gray-500) !important;
271
  color: var(--white) !important;
 
272
  }
273
 
274
  .btn-outline-secondary {
 
324
  .topic-facility { color: var(--gray-600) !important; font-weight: 600; }
325
  .topic-others { color: var(--gray-500) !important; font-weight: 600; }
326
 
327
+ /* Cards - Unified Gray Color Scheme */
328
+ .card-header {
329
+ background: var(--gray-400);
330
+ color: #F3F4F6;
 
 
 
 
 
 
 
 
 
331
  border-bottom: 1px solid var(--gray-300);
332
  }
333
 
334
+ .card-header.bg-primary {
335
+ background: var(--gray-400) !important;
336
+ color: #F3F4F6 !important;
337
+ }
338
+
339
+ .card-header h1,
340
+ .card-header h2,
341
+ .card-header h3,
342
+ .card-header h4,
343
+ .card-header h5,
344
+ .card-header h6,
345
+ .card-header .card-title {
346
+ color: #F3F4F6 !important;
347
+ }
348
+
349
  /* Button Container Spacing */
350
  .d-flex.gap-2 {
351
+ gap: 0.5rem;
352
+ margin-top: 0.75rem;
353
  }
354
 
355
  .d-flex.gap-2 .btn {
 
411
  }
412
 
413
  .alert {
414
+ border-radius: 4px;
415
  border: 1px solid var(--gray-200);
416
+ padding: 0.5rem 0.75rem;
417
  background: var(--white);
418
  transition: opacity 0.5s ease-out, transform 0.5s ease-out;
419
+ font-size: 0.875rem;
420
+ margin-bottom: 0.5rem;
421
+ max-width: 100%;
422
+ line-height: 1.4;
423
+ display: flex;
424
+ align-items: center;
425
+ justify-content: space-between;
426
+ min-height: 2.5rem;
427
  }
428
 
429
  .alert.fade-out {
 
431
  transform: translateY(-10px);
432
  }
433
 
434
+ /* Compact alert styles */
435
+ .alert .btn-close {
436
+ padding: 0;
437
+ margin: 0;
438
+ width: 1rem;
439
+ height: 1rem;
440
+ font-size: 0.75rem;
441
+ line-height: 1;
442
+ position: relative;
443
+ top: 0;
444
+ right: 0;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ border: none;
449
+ background: transparent;
450
+ opacity: 0.5;
451
+ transition: opacity 0.15s ease-in-out;
452
+ }
453
+
454
+ .alert .btn-close:hover {
455
+ opacity: 1;
456
+ }
457
+
458
+ .alert .btn-close::before {
459
+ content: "×";
460
+ font-size: 1.2rem;
461
+ font-weight: bold;
462
+ line-height: 1;
463
+ }
464
+
465
+ .alert i {
466
+ margin-right: 0.5rem;
467
+ font-size: 0.875rem;
468
+ display: inline-flex;
469
+ align-items: center;
470
+ flex-shrink: 0;
471
+ }
472
+
473
+ .alert .alert-content {
474
+ flex: 1;
475
+ display: flex;
476
+ align-items: center;
477
+ }
478
+
479
+ /* Alert positioning */
480
+ .alert-success {
481
+ background-color: rgba(16, 185, 129, 0.1);
482
+ border-color: rgba(16, 185, 129, 0.2);
483
+ color: #065f46;
484
+ }
485
+
486
+ .alert-info {
487
+ background-color: rgba(107, 114, 128, 0.1);
488
+ border-color: rgba(107, 114, 128, 0.2);
489
+ color: #374151;
490
+ }
491
+
492
+ .alert-warning {
493
+ background-color: rgba(245, 158, 11, 0.1);
494
+ border-color: rgba(245, 158, 11, 0.2);
495
+ color: #92400e;
496
+ }
497
+
498
+ .alert-danger {
499
+ background-color: rgba(239, 68, 68, 0.1);
500
+ border-color: rgba(239, 68, 68, 0.2);
501
+ color: #991b1b;
502
+ }
503
+
504
+ /* Specific positioning for main page alerts */
505
+ .row.justify-content-center.mb-2 {
506
+ margin-top: 0.5rem;
507
+ }
508
+
509
+ /* Compact spacing for alerts */
510
+ .alert + .alert {
511
+ margin-top: 0.25rem;
512
+ }
513
+
514
  blockquote {
515
  border-left: 4px solid var(--primary-color);
516
  padding-left: 1.5rem;
 
530
  }
531
 
532
  /* Bootstrap Overrides */
533
+ .bg-primary {
534
+ background: var(--gray-400);
535
+ }
536
+
537
+ .text-primary {
538
+ color: #F3F4F6 !important;
539
  }
540
 
 
541
  .spinner-border.text-primary {
542
+ color: #F3F4F6 !important;
543
  }
544
 
545
  .border-primary {
546
+ border-color: var(--primary-color);
547
+ }
548
+
549
+ /* Ensure all Bootstrap primary elements use gray */
550
+ .btn-primary,
551
+ .card-header.bg-primary,
552
+ .alert-primary {
553
+ background: var(--gray-400);
554
+ border-color: var(--gray-400);
555
+ color: #F3F4F6;
556
+ }
557
+
558
+ /* Force all primary buttons to never use blue */
559
+ .btn-primary,
560
+ .btn-primary:link,
561
+ .btn-primary:visited,
562
+ .btn-primary:hover,
563
+ .btn-primary:focus,
564
+ .btn-primary:active,
565
+ .btn-primary:not(:disabled):not(.disabled):active,
566
+ .btn-primary:not(:disabled):not(.disabled).active {
567
+ background: var(--gray-400) !important;
568
+ border-color: var(--gray-400) !important;
569
+ color: #F3F4F6 !important;
570
+ }
571
+
572
+ .btn-primary:hover,
573
+ .btn-primary:focus,
574
+ .btn-primary:active {
575
+ background: var(--gray-500) !important;
576
+ border-color: var(--gray-500) !important;
577
+ }
578
+
579
+ /* Force all text-primary to use light gray */
580
+ .text-primary,
581
+ h1.text-primary,
582
+ h2.text-primary,
583
+ h3.text-primary,
584
+ h4.text-primary,
585
+ h5.text-primary,
586
+ h6.text-primary,
587
+ .display-4.text-primary,
588
+ .fw-bold.text-primary {
589
+ color: #F3F4F6 !important;
590
+ }
591
+
592
+ /* Force all icons to use light gray */
593
+ .text-primary i,
594
+ .text-primary .fas,
595
+ .text-primary .fa {
596
+ color: #F3F4F6 !important;
597
  }
598
 
599
  /* Animations */
 
665
  background: rgba(135, 206, 235, 0.8);
666
  }
667
 
668
+ /* Pagination Styles - Gray Theme */
669
+ .pagination {
670
+ margin: 0;
671
+ padding: 0;
672
+ display: flex;
673
+ list-style: none;
674
+ border-radius: 8px;
675
+ overflow: hidden;
676
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
677
+ border: 1px solid var(--gray-200);
678
+ }
679
+
680
+ .pagination .page-item {
681
+ margin: 0;
682
+ border-right: 1px solid var(--gray-200);
683
+ }
684
+
685
+ .pagination .page-item:last-child {
686
+ border-right: none;
687
+ }
688
+
689
+ .pagination .page-item:first-child .page-link {
690
+ border-top-left-radius: 8px;
691
+ border-bottom-left-radius: 8px;
692
+ }
693
+
694
+ .pagination .page-item:last-child .page-link {
695
+ border-top-right-radius: 8px;
696
+ border-bottom-right-radius: 8px;
697
+ }
698
+
699
+ .pagination .page-link {
700
+ display: block;
701
+ padding: 0.5rem 0.75rem;
702
+ text-decoration: none;
703
+ color: var(--gray-700);
704
+ background-color: var(--white);
705
+ border: none;
706
+ font-weight: 500;
707
+ font-size: 0.9rem;
708
+ transition: all 0.2s ease;
709
+ min-width: 40px;
710
+ text-align: center;
711
+ }
712
+
713
+ .pagination .page-link:hover {
714
+ color: var(--white);
715
+ background-color: var(--gray-500);
716
+ text-decoration: none;
717
+ }
718
+
719
+ .pagination .page-item.active .page-link {
720
+ color: var(--white);
721
+ background-color: var(--gray-400);
722
+ font-weight: 600;
723
+ }
724
+
725
+ .pagination .page-item.active .page-link:hover {
726
+ background-color: var(--gray-500);
727
+ }
728
+
729
+ .pagination .page-item.disabled .page-link {
730
+ color: var(--gray-400);
731
+ background-color: var(--gray-100);
732
+ cursor: not-allowed;
733
+ }
734
+
735
+ .pagination .page-item.disabled .page-link:hover {
736
+ color: var(--gray-400);
737
+ background-color: var(--gray-100);
738
+ }
739
+
740
+ /* Navigation Links */
741
+ a[href*="login"]:hover,
742
+ a[href*="register"]:hover {
743
+ color: var(--gray-300);
744
+ text-decoration: underline;
745
+ transform: translateY(-1px);
746
+ transition: all 0.3s ease;
747
+ }
748
+
749
+ /* Responsive Design */
750
  @media (max-width: 768px) {
751
+ .container-fluid {
752
+ margin: 0 10px 10px 0;
753
+ }
754
 
755
  header {
756
  border-radius: 12px;
757
+ padding: 1rem;
758
  margin: 0 0 1rem 0;
759
  }
760
 
761
+ header h1 {
762
+ font-size: 1.8rem;
763
+ }
764
 
765
+ .card-body {
766
+ padding: 1.25rem;
767
+ }
768
 
769
  .btn {
770
  padding: 0.75rem 1.5rem;
 
772
  }
773
 
774
  .card-header {
775
+ border-radius: 12px 12px 0 0;
776
  padding: 1rem;
777
  }
778
 
 
785
  padding: 0.4rem 0.8rem;
786
  font-size: 0.85rem;
787
  }
788
+
789
+ .pagination .page-link {
790
+ padding: 0.4rem 0.6rem;
791
+ font-size: 0.85rem;
792
+ min-width: 35px;
793
+ }
794
+ }
795
+
796
+ /* Glass Button Component */
797
+ .btn-glass {
798
+ background-color: var(--glass-bg);
799
+ border: 1px solid var(--glass-border);
800
+ color: #fff;
801
+ padding: 8px;
802
+ border-radius: 6px;
803
+ text-decoration: none;
804
+ font-weight: 600;
805
+ display: inline-flex;
806
+ align-items: center;
807
+ justify-content: center;
808
+ font-size: 13px;
809
+ gap: 6px;
810
+ backdrop-filter: blur(10px);
811
+ transition: all 0.3s ease;
812
+ }
813
+
814
+ .btn-glass:hover {
815
+ background-color: var(--glass-hover-bg);
816
+ transform: translateY(-2px);
817
+ box-shadow: var(--glass-shadow);
818
+ color: #fff;
819
+ }
820
+
821
+ /* Link hover effects for login/register pages */
822
+ a.text-secondary.fw-bold:hover {
823
+ color: #3B82F6 !important;
824
+ transition: color 0.3s ease;
825
+ }
826
+
827
+ .btn-glass:active {
828
+ transform: translateY(0);
829
+ box-shadow: none;
830
+ }
831
+
static/js/app.js CHANGED
@@ -1,5 +1,30 @@
1
  // Student Feedback Analysis - Optimized JavaScript
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  document.addEventListener('DOMContentLoaded', function() {
4
  // DOM Elements
5
  const elements = {
@@ -21,51 +46,16 @@ document.addEventListener('DOMContentLoaded', function() {
21
 
22
  // Configuration Objects
23
  const sentimentConfigs = {
24
- positive: {
25
- icon: 'fa-smile',
26
- label: 'Tích Cực',
27
- colorClass: 'sentiment-positive',
28
- description: 'Feedback thể hiện thái độ tích cực và hài lòng'
29
- },
30
- neutral: {
31
- icon: 'fa-meh',
32
- label: 'Trung Tính',
33
- colorClass: 'sentiment-neutral',
34
- description: 'Feedback thể hiện thái độ trung lập, không rõ ràng'
35
- },
36
- negative: {
37
- icon: 'fa-frown',
38
- label: 'Tiêu Cực',
39
- colorClass: 'sentiment-negative',
40
- description: 'Feedback thể hiện thái độ tiêu cực và không hài lòng'
41
- }
42
  };
43
 
44
  const topicConfigs = {
45
- lecturer: {
46
- icon: 'fa-user-tie',
47
- label: 'Giảng Viên',
48
- colorClass: 'topic-lecturer',
49
- description: 'Feedback liên quan đến chất lượng giảng dạy của giảng viên'
50
- },
51
- training_program: {
52
- icon: 'fa-graduation-cap',
53
- label: 'Chương Trình Đào Tạo',
54
- colorClass: 'topic-training_program',
55
- description: 'Feedback về nội dung và cấu trúc chương trình học'
56
- },
57
- facility: {
58
- icon: 'fa-building',
59
- label: 'Cơ Sở Vật Chất',
60
- colorClass: 'topic-facility',
61
- description: 'Feedback về phòng học, thiết bị và cơ sở hạ tầng'
62
- },
63
- others: {
64
- icon: 'fa-ellipsis-h',
65
- label: 'Khác',
66
- colorClass: 'topic-others',
67
- description: 'Feedback về các chủ đề khác không thuộc các danh mục trên'
68
- }
69
  };
70
 
71
  const examples = [
@@ -88,13 +78,11 @@ document.addEventListener('DOMContentLoaded', function() {
88
  elements.analyzeBtn.disabled = true;
89
  elements.analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Đang phân tích...';
90
  },
91
-
92
  hideLoading() {
93
  elements.loadingSpinner.style.display = 'none';
94
  elements.analyzeBtn.disabled = false;
95
  elements.analyzeBtn.innerHTML = '<i class="fas fa-search me-2"></i>Phân Tích Feedback';
96
  },
97
-
98
  showError(message) {
99
  elements.errorText.textContent = message;
100
  elements.errorMessage.style.display = 'block';
@@ -102,50 +90,44 @@ document.addEventListener('DOMContentLoaded', function() {
102
  elements.results.style.display = 'none';
103
  elements.errorMessage.scrollIntoView({ behavior: 'smooth' });
104
  },
105
-
106
  clearForm() {
107
  elements.textarea.value = '';
108
  elements.results.style.display = 'none';
109
  elements.errorMessage.style.display = 'none';
110
  this.updateCharCounter();
 
 
 
 
 
 
 
 
 
 
111
  },
112
-
113
  updateResult(type, value) {
114
  const configs = type === 'sentiment' ? sentimentConfigs : topicConfigs;
115
  const config = configs[value] || configs[type === 'sentiment' ? 'neutral' : 'others'];
116
-
117
  const resultEl = elements[`${type}Result`];
118
  const iconEl = elements[`${type}Icon`];
119
  const descEl = elements[`${type}Description`];
120
-
121
  resultEl.className = `mb-2 ${config.colorClass}`;
122
  iconEl.innerHTML = `<i class="fas ${config.icon} fa-3x ${config.colorClass}"></i>`;
123
  resultEl.textContent = config.label;
124
  descEl.textContent = config.description;
125
  },
126
-
127
  scrollToResults() {
128
- setTimeout(() => {
129
- const resultsRect = elements.results.getBoundingClientRect();
130
- const windowHeight = window.innerHeight;
131
- const scrollOffset = window.scrollY + resultsRect.top - (windowHeight * 0.1);
132
-
133
- window.scrollTo({
134
- top: Math.max(0, scrollOffset),
135
- behavior: 'smooth'
136
- });
137
- }, 100);
138
  },
139
-
140
  updateCharCounter() {
141
  const count = elements.textarea.value.length;
142
  const charCount = document.getElementById('charCount');
143
  const charCounter = document.querySelector('.form-text');
144
-
145
  if (charCount) charCount.textContent = count;
146
  if (charCounter) {
147
- charCounter.style.color = count > 500 ? '#dc3545' :
148
- count > 300 ? '#ffc107' : '#6c757d';
149
  }
150
  }
151
  };
@@ -153,25 +135,19 @@ document.addEventListener('DOMContentLoaded', function() {
153
  // Main Functions
154
  async function handleFormSubmit(e) {
155
  e.preventDefault();
156
-
157
  const feedbackText = elements.textarea.value.trim();
158
-
159
  if (!feedbackText) {
160
  utils.showError('Vui lòng nhập feedback trước khi phân tích!');
161
  return;
162
  }
163
-
164
  utils.showLoading();
165
-
166
  try {
167
  const response = await fetch('/predict', {
168
  method: 'POST',
169
  headers: { 'Content-Type': 'application/json' },
170
  body: JSON.stringify({ text: feedbackText })
171
  });
172
-
173
  const data = await response.json();
174
-
175
  if (response.ok) {
176
  utils.updateResult('sentiment', data.sentiment);
177
  utils.updateResult('topic', data.topic);
@@ -180,6 +156,10 @@ document.addEventListener('DOMContentLoaded', function() {
180
  elements.results.classList.add('fade-in');
181
  utils.scrollToResults();
182
 
 
 
 
 
183
  // Reload feedback history after successful analysis
184
  loadFeedbackHistory(1);
185
  } else {
@@ -197,13 +177,21 @@ document.addEventListener('DOMContentLoaded', function() {
197
  const randomExample = examples[Math.floor(Math.random() * examples.length)];
198
  elements.textarea.value = randomExample;
199
  elements.textarea.dispatchEvent(new Event('input'));
200
- elements.form.scrollIntoView({ behavior: 'smooth' });
 
 
 
 
 
 
 
 
 
201
  }
202
 
203
  // Event Listeners
204
  elements.form.addEventListener('submit', handleFormSubmit);
205
  elements.textarea.addEventListener('input', utils.updateCharCounter);
206
-
207
  document.addEventListener('keydown', function(e) {
208
  if (e.ctrlKey && e.key === 'Enter') {
209
  elements.form.dispatchEvent(new Event('submit'));
@@ -215,19 +203,16 @@ document.addEventListener('DOMContentLoaded', function() {
215
  // Add utility buttons
216
  const buttonContainer = document.createElement('div');
217
  buttonContainer.className = 'd-flex justify-content-center gap-2 mt-3';
218
-
219
  const clearBtn = document.createElement('button');
220
  clearBtn.type = 'button';
221
  clearBtn.className = 'btn btn-outline-secondary';
222
  clearBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Xóa Form';
223
  clearBtn.onclick = () => utils.clearForm();
224
-
225
  const exampleBtn = document.createElement('button');
226
  exampleBtn.type = 'button';
227
  exampleBtn.className = 'btn btn-outline-info';
228
  exampleBtn.innerHTML = '<i class="fas fa-lightbulb me-2"></i>Ví Dụ';
229
  exampleBtn.onclick = showExamples;
230
-
231
  buttonContainer.appendChild(clearBtn);
232
  buttonContainer.appendChild(exampleBtn);
233
  elements.form.appendChild(buttonContainer);
@@ -240,28 +225,34 @@ document.addEventListener('DOMContentLoaded', function() {
240
 
241
  // Load feedback history
242
  loadFeedbackHistory();
 
 
 
243
  });
244
 
245
- // Feedback History Functions
246
  let currentPage = 1;
247
  const itemsPerPage = 5;
248
 
249
- async function loadFeedbackHistory(page = 1) {
250
  const historyLoading = document.getElementById('historyLoading');
251
  const historyContent = document.getElementById('historyContent');
252
  const historyPagination = document.getElementById('historyPagination');
253
-
 
254
  historyLoading.style.display = 'block';
255
- historyContent.innerHTML = '';
256
- historyPagination.innerHTML = '';
257
-
258
  try {
259
  const response = await fetch(`/api/feedback-history?page=${page}&per_page=${itemsPerPage}`);
260
  const data = await response.json();
261
-
262
  if (response.ok) {
263
  displayFeedbackHistory(data.feedbacks);
264
  displayPagination(data, page);
 
 
 
 
265
  } else {
266
  historyContent.innerHTML = '<p class="text-muted text-center">Không thể tải lịch sử feedback.</p>';
267
  }
@@ -270,23 +261,23 @@ async function loadFeedbackHistory(page = 1) {
270
  historyContent.innerHTML = '<p class="text-muted text-center">Có lỗi xảy ra khi tải lịch sử.</p>';
271
  } finally {
272
  historyLoading.style.display = 'none';
 
 
 
273
  }
274
  }
275
 
276
  function displayFeedbackHistory(feedbacks) {
277
  const historyContent = document.getElementById('historyContent');
278
-
279
- if (feedbacks.length === 0) {
280
  historyContent.innerHTML = '<p class="text-muted text-center">Chưa có feedback nào. Hãy phân tích feedback đầu tiên!</p>';
281
  return;
282
  }
283
-
284
  let html = '';
285
  feedbacks.forEach(feedback => {
286
  const sentimentConfig = getSentimentConfig(feedback.sentiment);
287
  const topicConfig = getTopicConfig(feedback.topic);
288
  const date = new Date(feedback.created_at).toLocaleString('vi-VN');
289
-
290
  html += `
291
  <div class="card mb-3 border-start border-4 border-${getSentimentColor(feedback.sentiment)}">
292
  <div class="card-body">
@@ -318,33 +309,32 @@ function displayFeedbackHistory(feedbacks) {
318
  </div>
319
  `;
320
  });
321
-
322
  historyContent.innerHTML = html;
323
  }
324
 
325
  function displayPagination(data, currentPage) {
326
  const historyPagination = document.getElementById('historyPagination');
327
-
328
- if (data.pages <= 1) return;
329
-
 
330
  let html = '<nav><ul class="pagination pagination-sm">';
331
-
332
- // Previous button
333
  if (data.has_prev) {
334
- html += `<li class="page-item"><a class="page-link" href="#" onclick="loadFeedbackHistory(${currentPage - 1})">Trước</a></li>`;
 
 
335
  }
336
-
337
- // Page numbers
338
  for (let i = 1; i <= data.pages; i++) {
339
  const active = i === currentPage ? 'active' : '';
340
- html += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="loadFeedbackHistory(${i})">${i}</a></li>`;
 
 
341
  }
342
-
343
- // Next button
344
  if (data.has_next) {
345
- html += `<li class="page-item"><a class="page-link" href="#" onclick="loadFeedbackHistory(${currentPage + 1})">Sau</a></li>`;
 
 
346
  }
347
-
348
  html += '</ul></nav>';
349
  historyPagination.innerHTML = html;
350
  }
@@ -352,7 +342,7 @@ function displayPagination(data, currentPage) {
352
  function getSentimentConfig(sentiment) {
353
  const configs = {
354
  positive: { icon: 'fa-smile', label: 'Tích Cực' },
355
- neutral: { icon: 'fa-meh', label: 'Trung Tính' },
356
  negative: { icon: 'fa-frown', label: 'Tiêu Cực' }
357
  };
358
  return configs[sentiment] || configs.neutral;
@@ -360,19 +350,15 @@ function getSentimentConfig(sentiment) {
360
 
361
  function getTopicConfig(topic) {
362
  const configs = {
363
- lecturer: { icon: 'fa-user-tie', label: 'Giảng Viên' },
364
  training_program: { icon: 'fa-graduation-cap', label: 'Chương Trình' },
365
- facility: { icon: 'fa-building', label: 'Cơ Sở Vật Chất' },
366
- others: { icon: 'fa-ellipsis-h', label: 'Khác' }
367
  };
368
  return configs[topic] || configs.others;
369
  }
370
 
371
  function getSentimentColor(sentiment) {
372
- const colors = {
373
- positive: 'success',
374
- neutral: 'warning',
375
- negative: 'danger'
376
- };
377
  return colors[sentiment] || 'secondary';
378
- }
 
1
  // Student Feedback Analysis - Optimized JavaScript
2
 
3
+ // ===== Global Utilities =====
4
+ const Utils = {
5
+ // Smooth scrolling with automatic offset calculation
6
+ scrollToSection(el, extraOffset = 0) {
7
+ if (!el) return;
8
+ const fixed = document.querySelector('.navbar.fixed-top, header.sticky-top, .sticky-top, .fixed-top');
9
+ const base = fixed ? Math.max(0, fixed.getBoundingClientRect().height + 8) : 56;
10
+ const offset = Math.max(0, base + extraOffset);
11
+ const rect = el.getBoundingClientRect();
12
+ const top = window.pageYOffset + rect.top - offset;
13
+ window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
14
+ },
15
+
16
+ // Auto-hide alerts functionality
17
+ initAutoHideAlerts() {
18
+ const alerts = document.querySelectorAll('.alert');
19
+ alerts.forEach(alert => {
20
+ setTimeout(() => {
21
+ alert.classList.add('fade-out');
22
+ setTimeout(() => alert.remove(), 500);
23
+ }, 2000);
24
+ });
25
+ }
26
+ };
27
+
28
  document.addEventListener('DOMContentLoaded', function() {
29
  // DOM Elements
30
  const elements = {
 
46
 
47
  // Configuration Objects
48
  const sentimentConfigs = {
49
+ positive: { icon: 'fa-smile', label: 'Tích Cực', colorClass: 'sentiment-positive', description: 'Feedback thể hiện thái độ tích cực và hài lòng' },
50
+ neutral: { icon: 'fa-meh', label: 'Trung Tính', colorClass: 'sentiment-neutral', description: 'Feedback thể hiện thái độ trung lập, không rõ ràng' },
51
+ negative: { icon: 'fa-frown', label: 'Tiêu Cực', colorClass: 'sentiment-negative', description: 'Feedback thể hiện thái độ tiêu cực và không hài lòng' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  };
53
 
54
  const topicConfigs = {
55
+ lecturer: { icon: 'fa-user-tie', label: 'Giảng Viên', colorClass: 'topic-lecturer', description: 'Feedback liên quan đến chất lượng giảng dạy của giảng viên' },
56
+ training_program: { icon: 'fa-graduation-cap', label: 'Chương Trình Đào Tạo', colorClass: 'topic-training_program', description: 'Feedback về nội dung và cấu trúc chương trình học' },
57
+ facility: { icon: 'fa-building', label: ' Sở Vật Chất', colorClass: 'topic-facility', description: 'Feedback về phòng học, thiết bị và cơ sở hạ tầng' },
58
+ others: { icon: 'fa-ellipsis-h', label: 'Khác', colorClass: 'topic-others', description: 'Feedback về các chủ đề khác không thuộc các danh mục trên' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  };
60
 
61
  const examples = [
 
78
  elements.analyzeBtn.disabled = true;
79
  elements.analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Đang phân tích...';
80
  },
 
81
  hideLoading() {
82
  elements.loadingSpinner.style.display = 'none';
83
  elements.analyzeBtn.disabled = false;
84
  elements.analyzeBtn.innerHTML = '<i class="fas fa-search me-2"></i>Phân Tích Feedback';
85
  },
 
86
  showError(message) {
87
  elements.errorText.textContent = message;
88
  elements.errorMessage.style.display = 'block';
 
90
  elements.results.style.display = 'none';
91
  elements.errorMessage.scrollIntoView({ behavior: 'smooth' });
92
  },
 
93
  clearForm() {
94
  elements.textarea.value = '';
95
  elements.results.style.display = 'none';
96
  elements.errorMessage.style.display = 'none';
97
  this.updateCharCounter();
98
+
99
+ // Cuộn lên form giống như khi nhấn "Ví Dụ"
100
+ const formElement = elements.form;
101
+ const formRect = formElement.getBoundingClientRect();
102
+ const scrollTop = window.pageYOffset + formRect.top - 120; // Cùng vị trí như "Ví Dụ"
103
+
104
+ window.scrollTo({
105
+ top: scrollTop,
106
+ behavior: 'smooth'
107
+ });
108
  },
 
109
  updateResult(type, value) {
110
  const configs = type === 'sentiment' ? sentimentConfigs : topicConfigs;
111
  const config = configs[value] || configs[type === 'sentiment' ? 'neutral' : 'others'];
 
112
  const resultEl = elements[`${type}Result`];
113
  const iconEl = elements[`${type}Icon`];
114
  const descEl = elements[`${type}Description`];
 
115
  resultEl.className = `mb-2 ${config.colorClass}`;
116
  iconEl.innerHTML = `<i class="fas ${config.icon} fa-3x ${config.colorClass}"></i>`;
117
  resultEl.textContent = config.label;
118
  descEl.textContent = config.description;
119
  },
 
120
  scrollToResults() {
121
+ Utils.scrollToSection(elements.results, 105);
 
 
 
 
 
 
 
 
 
122
  },
 
123
  updateCharCounter() {
124
  const count = elements.textarea.value.length;
125
  const charCount = document.getElementById('charCount');
126
  const charCounter = document.querySelector('.form-text');
 
127
  if (charCount) charCount.textContent = count;
128
  if (charCounter) {
129
+ charCounter.style.color = count > 500 ? '#dc3545' :
130
+ count > 300 ? '#ffc107' : '#6c757d';
131
  }
132
  }
133
  };
 
135
  // Main Functions
136
  async function handleFormSubmit(e) {
137
  e.preventDefault();
 
138
  const feedbackText = elements.textarea.value.trim();
 
139
  if (!feedbackText) {
140
  utils.showError('Vui lòng nhập feedback trước khi phân tích!');
141
  return;
142
  }
 
143
  utils.showLoading();
 
144
  try {
145
  const response = await fetch('/predict', {
146
  method: 'POST',
147
  headers: { 'Content-Type': 'application/json' },
148
  body: JSON.stringify({ text: feedbackText })
149
  });
 
150
  const data = await response.json();
 
151
  if (response.ok) {
152
  utils.updateResult('sentiment', data.sentiment);
153
  utils.updateResult('topic', data.topic);
 
156
  elements.results.classList.add('fade-in');
157
  utils.scrollToResults();
158
 
159
+ // Clear the textarea after successful analysis
160
+ elements.textarea.value = '';
161
+ utils.updateCharCounter();
162
+
163
  // Reload feedback history after successful analysis
164
  loadFeedbackHistory(1);
165
  } else {
 
177
  const randomExample = examples[Math.floor(Math.random() * examples.length)];
178
  elements.textarea.value = randomExample;
179
  elements.textarea.dispatchEvent(new Event('input'));
180
+
181
+ // Cuộn trực tiếp với vị trí cao hơn
182
+ const formElement = elements.form;
183
+ const formRect = formElement.getBoundingClientRect();
184
+ const scrollTop = window.pageYOffset + formRect.top - 120; // Tăng lên 120px để cuộn cao hơn
185
+
186
+ window.scrollTo({
187
+ top: scrollTop,
188
+ behavior: 'smooth'
189
+ });
190
  }
191
 
192
  // Event Listeners
193
  elements.form.addEventListener('submit', handleFormSubmit);
194
  elements.textarea.addEventListener('input', utils.updateCharCounter);
 
195
  document.addEventListener('keydown', function(e) {
196
  if (e.ctrlKey && e.key === 'Enter') {
197
  elements.form.dispatchEvent(new Event('submit'));
 
203
  // Add utility buttons
204
  const buttonContainer = document.createElement('div');
205
  buttonContainer.className = 'd-flex justify-content-center gap-2 mt-3';
 
206
  const clearBtn = document.createElement('button');
207
  clearBtn.type = 'button';
208
  clearBtn.className = 'btn btn-outline-secondary';
209
  clearBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Xóa Form';
210
  clearBtn.onclick = () => utils.clearForm();
 
211
  const exampleBtn = document.createElement('button');
212
  exampleBtn.type = 'button';
213
  exampleBtn.className = 'btn btn-outline-info';
214
  exampleBtn.innerHTML = '<i class="fas fa-lightbulb me-2"></i>Ví Dụ';
215
  exampleBtn.onclick = showExamples;
 
216
  buttonContainer.appendChild(clearBtn);
217
  buttonContainer.appendChild(exampleBtn);
218
  elements.form.appendChild(buttonContainer);
 
225
 
226
  // Load feedback history
227
  loadFeedbackHistory();
228
+
229
+ // Initialize auto-hide alerts
230
+ Utils.initAutoHideAlerts();
231
  });
232
 
233
+ // ===== Feedback History Functions =====
234
  let currentPage = 1;
235
  const itemsPerPage = 5;
236
 
237
+ async function loadFeedbackHistory(page = 1, shouldScroll = false) {
238
  const historyLoading = document.getElementById('historyLoading');
239
  const historyContent = document.getElementById('historyContent');
240
  const historyPagination = document.getElementById('historyPagination');
241
+ const feedbackHistorySection = document.getElementById('feedbackHistory');
242
+
243
  historyLoading.style.display = 'block';
244
+ historyContent.style.opacity = '0.5';
245
+ historyPagination.style.opacity = '0.5';
 
246
  try {
247
  const response = await fetch(`/api/feedback-history?page=${page}&per_page=${itemsPerPage}`);
248
  const data = await response.json();
 
249
  if (response.ok) {
250
  displayFeedbackHistory(data.feedbacks);
251
  displayPagination(data, page);
252
+ if (shouldScroll && feedbackHistorySection) {
253
+ // Pagination cuộn thấp xuống một chút
254
+ Utils.scrollToSection(feedbackHistorySection, -40);
255
+ }
256
  } else {
257
  historyContent.innerHTML = '<p class="text-muted text-center">Không thể tải lịch sử feedback.</p>';
258
  }
 
261
  historyContent.innerHTML = '<p class="text-muted text-center">Có lỗi xảy ra khi tải lịch sử.</p>';
262
  } finally {
263
  historyLoading.style.display = 'none';
264
+ historyContent.style.opacity = '1';
265
+ historyPagination.style.opacity = '1';
266
+ currentPage = page;
267
  }
268
  }
269
 
270
  function displayFeedbackHistory(feedbacks) {
271
  const historyContent = document.getElementById('historyContent');
272
+ if (!feedbacks || feedbacks.length === 0) {
 
273
  historyContent.innerHTML = '<p class="text-muted text-center">Chưa có feedback nào. Hãy phân tích feedback đầu tiên!</p>';
274
  return;
275
  }
 
276
  let html = '';
277
  feedbacks.forEach(feedback => {
278
  const sentimentConfig = getSentimentConfig(feedback.sentiment);
279
  const topicConfig = getTopicConfig(feedback.topic);
280
  const date = new Date(feedback.created_at).toLocaleString('vi-VN');
 
281
  html += `
282
  <div class="card mb-3 border-start border-4 border-${getSentimentColor(feedback.sentiment)}">
283
  <div class="card-body">
 
309
  </div>
310
  `;
311
  });
 
312
  historyContent.innerHTML = html;
313
  }
314
 
315
  function displayPagination(data, currentPage) {
316
  const historyPagination = document.getElementById('historyPagination');
317
+ if (!data || data.pages <= 1) {
318
+ historyPagination.innerHTML = '';
319
+ return;
320
+ }
321
  let html = '<nav><ul class="pagination pagination-sm">';
 
 
322
  if (data.has_prev) {
323
+ html += `<li class="page-item">
324
+ <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${currentPage - 1}, true); return false;">Trước</a>
325
+ </li>`;
326
  }
 
 
327
  for (let i = 1; i <= data.pages; i++) {
328
  const active = i === currentPage ? 'active' : '';
329
+ html += `<li class="page-item ${active}">
330
+ <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${i}, true); return false;">${i}</a>
331
+ </li>`;
332
  }
 
 
333
  if (data.has_next) {
334
+ html += `<li class="page-item">
335
+ <a class="page-link" href="javascript:void(0)" onclick="loadFeedbackHistory(${currentPage + 1}, true); return false;">Sau</a>
336
+ </li>`;
337
  }
 
338
  html += '</ul></nav>';
339
  historyPagination.innerHTML = html;
340
  }
 
342
  function getSentimentConfig(sentiment) {
343
  const configs = {
344
  positive: { icon: 'fa-smile', label: 'Tích Cực' },
345
+ neutral: { icon: 'fa-meh', label: 'Trung Tính' },
346
  negative: { icon: 'fa-frown', label: 'Tiêu Cực' }
347
  };
348
  return configs[sentiment] || configs.neutral;
 
350
 
351
  function getTopicConfig(topic) {
352
  const configs = {
353
+ lecturer: { icon: 'fa-user-tie', label: 'Giảng Viên' },
354
  training_program: { icon: 'fa-graduation-cap', label: 'Chương Trình' },
355
+ facility: { icon: 'fa-building', label: 'Cơ Sở Vật Chất' },
356
+ others: { icon: 'fa-ellipsis-h', label: 'Khác' }
357
  };
358
  return configs[topic] || configs.others;
359
  }
360
 
361
  function getSentimentColor(sentiment) {
362
+ const colors = { positive: 'success', neutral: 'warning', negative: 'danger' };
 
 
 
 
363
  return colors[sentiment] || 'secondary';
364
+ }
templates/.DS_Store ADDED
Binary file (6.15 kB). View file
 
templates/base.html ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Student Feedback Analysis{% endblock %}</title>
7
+
8
+ <!-- CSS Dependencies -->
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
11
+ <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
12
+
13
+ {% block extra_head %}{% endblock %}
14
+ </head>
15
+ <body>
16
+ <div class="container-fluid{% block container_class %}{% endblock %}">
17
+ <!-- Header -->
18
+ {% block header %}
19
+ <header>
20
+ <div class="d-flex justify-content-between align-items-center">
21
+ <h1 class="display-4 fw-bold text-primary mb-0">
22
+ <i class="fas fa-graduation-cap me-1" style="margin-left: 10px !important;"></i>
23
+ {% block page_title %}Student Feedback Analysis{% endblock %}
24
+ </h1>
25
+ <div class="d-flex align-items-center justify-content-end" style="margin-right: 20px; gap: 16px;">
26
+ {% if current_user.is_authenticated %}
27
+ <span class="text-white fw-semibold d-flex align-items-center">
28
+ <i class="fas fa-user me-2"></i>
29
+ {{ current_user.username }}
30
+ </span>
31
+
32
+ {% block navigation_buttons %}
33
+ {% if current_user.is_admin %}
34
+ <a href="{{ url_for('view_database') }}" class="btn-glass">
35
+ <i class="fas fa-database"></i>
36
+ <span>Database</span>
37
+ </a>
38
+ {% else %}
39
+ <a href="{{ url_for('my_statistics') }}" class="btn-glass">
40
+ <i class="fas fa-chart-bar"></i>
41
+ <span>Thống kê của tôi</span>
42
+ </a>
43
+ {% endif %}
44
+ <a href="{{ url_for('home') }}" class="btn-glass">
45
+ <i class="fas fa-chart-line"></i>
46
+ <span>Trang Phân Tích</span>
47
+ </a>
48
+ <a href="{{ url_for('logout') }}" class="btn-glass">
49
+ <i class="fas fa-sign-out-alt"></i>
50
+ <span>Đăng xuất</span>
51
+ </a>
52
+ {% endblock %}
53
+ {% endif %}
54
+ </div>
55
+ </div>
56
+ </header>
57
+ {% endblock %}
58
+
59
+ <!-- Flash Messages -->
60
+ {% block flash_messages %}
61
+ {% with messages = get_flashed_messages(with_categories=true) %}
62
+ {% if messages %}
63
+ <div class="row justify-content-center mb-2">
64
+ <div class="col-lg-8 col-md-10">
65
+ {% for category, message in messages %}
66
+ <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
67
+ <div class="alert-content">
68
+ <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' if category == 'info' else 'check-circle' if category == 'success' else 'exclamation-triangle' }}"></i>
69
+ {{ message }}
70
+ </div>
71
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
72
+ </div>
73
+ {% endfor %}
74
+ </div>
75
+ </div>
76
+ {% endif %}
77
+ {% endwith %}
78
+ {% endblock %}
79
+
80
+ <!-- Main Content -->
81
+ {% block content %}{% endblock %}
82
+
83
+ <!-- Footer -->
84
+ {% block footer %}
85
+ <footer class="text-center py-4 mt-5">
86
+ <p class="text-muted mb-0">
87
+ <i class="fas fa-robot me-2"></i>
88
+ Powered by Tư _ Đức _ Nghĩa _ Hà
89
+ </p>
90
+ </footer>
91
+ {% endblock %}
92
+ </div>
93
+
94
+ <!-- Scripts -->
95
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
96
+ {% block extra_scripts %}{% endblock %}
97
+
98
+ <!-- Common Auto-hide Alerts Script -->
99
+ <script>
100
+ document.addEventListener('DOMContentLoaded', function() {
101
+ const alerts = document.querySelectorAll('.alert');
102
+ alerts.forEach(function(alert) {
103
+ setTimeout(function() {
104
+ alert.classList.add('fade-out');
105
+ setTimeout(function() {
106
+ alert.remove();
107
+ }, 500);
108
+ }, 2000);
109
+ });
110
+ });
111
+ </script>
112
+ </body>
113
+ </html>
templates/database_view.html ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Xem Database - Student Feedback Analysis{% endblock %}
4
+ {% block page_title %}Quản Lý Database{% endblock %}
5
+ {% block footer %}{% endblock %}
6
+
7
+ {% block extra_head %}
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ /* Use global styles - remove custom database styling */
11
+ .table thead th { color: #374151; }
12
+ .table tbody td { color: #374151; }
13
+ </style>
14
+ {% endblock %}
15
+
16
+ {% block content %}
17
+ <!-- Thống kê tổng quan -->
18
+ <div class="row mb-4">
19
+ <div class="col-md-6">
20
+ <div class="card text-center">
21
+ <div class="card-body">
22
+ <i class="fas fa-users fa-3x mb-3" style="color: #3B82F6 !important;"></i>
23
+ <h3 class="card-title">{{ total_users }}</h3>
24
+ <p class="card-text">Tổng số người dùng</p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <div class="col-md-6">
29
+ <div class="card text-center">
30
+ <div class="card-body">
31
+ <i class="fas fa-comments fa-3x mb-3" style="color: #10B981 !important;"></i>
32
+ <h3 class="card-title">{{ total_feedbacks }}</h3>
33
+ <p class="card-text">Tổng số feedback</p>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Biểu đồ thống kê -->
40
+ <div class="row mb-4">
41
+ <div class="col-md-6">
42
+ <div class="card">
43
+ <div class="card-header">
44
+ <h5 class="mb-0"><i class="fas fa-chart-pie me-2" style="color: #F59E0B !important;"></i>Phân bố Sentiment</h5>
45
+ </div>
46
+ <div class="card-body">
47
+ <canvas id="sentimentChart" width="400" height="200"></canvas>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <div class="col-md-6">
52
+ <div class="card">
53
+ <div class="card-header">
54
+ <h5 class="mb-0"><i class="fas fa-chart-bar me-2" style="color: #F3F4F6 !important;"></i>Phân bố Topic</h5>
55
+ </div>
56
+ <div class="card-body">
57
+ <canvas id="topicChart" width="400" height="200"></canvas>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Feedback gần nhất -->
64
+ <div class="card mb-4">
65
+ <div class="card-header">
66
+ <h5 class="mb-0"><i class="fas fa-history me-2" style="color: #EF4444 !important;"></i>Feedback Gần Nhất</h5>
67
+ </div>
68
+ <div class="card-body">
69
+ {% if recent_feedbacks %}
70
+ <div class="table-responsive">
71
+ <table class="table table-striped">
72
+ <thead>
73
+ <tr>
74
+ <th>ID</th>
75
+ <th>User</th>
76
+ <th>Feedback</th>
77
+ <th>Sentiment</th>
78
+ <th>Topic</th>
79
+ <th>Thời gian</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {% for feedback in recent_feedbacks %}
84
+ <tr>
85
+ <td>{{ feedback.id }}</td>
86
+ <td>{{ feedback.user.username }}</td>
87
+ <td>
88
+ <div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
89
+ {{ feedback.text }}
90
+ </div>
91
+ </td>
92
+ <td>
93
+ <span class="badge bg-{{ 'success' if feedback.sentiment == 'positive' else 'warning' if feedback.sentiment == 'neutral' else 'danger' }}">
94
+ {{ feedback.sentiment }}
95
+ </span>
96
+ </td>
97
+ <td>
98
+ <span class="badge bg-secondary">{{ feedback.topic }}</span>
99
+ </td>
100
+ <td>{{ feedback.created_at.strftime('%d/%m/%Y %H:%M') }}</td>
101
+ </tr>
102
+ {% endfor %}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ {% else %}
107
+ <p class="text-muted text-center">Chưa có feedback nào.</p>
108
+ {% endif %}
109
+ </div>
110
+ </div>
111
+
112
+ <!-- Thống kê chi tiết -->
113
+ <div class="row">
114
+ <div class="col-md-6">
115
+ <div class="card">
116
+ <div class="card-header">
117
+ <h5 class="mb-0"><i class="fas fa-chart-line me-2" style="color: #06B6D4 !important;"></i>Feedback theo ngày (7 ngày gần nhất)</h5>
118
+ </div>
119
+ <div class="card-body">
120
+ <canvas id="dailyChart" width="400" height="200"></canvas>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ <div class="col-md-6">
125
+ <div class="card">
126
+ <div class="card-header">
127
+ <h5 class="mb-0"><i class="fas fa-info-circle me-2" style="color: #84CC16 !important;"></i>Thông tin Database</h5>
128
+ </div>
129
+ <div class="card-body">
130
+ <ul class="list-group list-group-flush">
131
+ <li class="list-group-item d-flex justify-content-between">
132
+ <span>Loại database:</span>
133
+ <span class="badge bg-primary">SQLite</span>
134
+ </li>
135
+ <li class="list-group-item d-flex justify-content-between">
136
+ <span>Bảng chính:</span>
137
+ <span>users, feedbacks</span>
138
+ </li>
139
+ <li class="list-group-item d-flex justify-content-between">
140
+ <span>Trạng thái:</span>
141
+ <span class="badge bg-success">Hoạt động</span>
142
+ </li>
143
+ </ul>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Hidden data elements -->
151
+ <div id="sentiment-data" style="display: none;">{{ sentiment_stats | tojson | safe }}</div>
152
+ <div id="topic-data" style="display: none;">{{ topic_stats | tojson | safe }}</div>
153
+ <div id="daily-data" style="display: none;">{{ daily_stats | tojson | safe }}</div>
154
+
155
+ {% endblock %}
156
+
157
+ {% block extra_scripts %}
158
+ <script>
159
+ // Initialize charts when page loads
160
+ document.addEventListener('DOMContentLoaded', function() {
161
+ // Get data from hidden elements
162
+ const sentimentData = JSON.parse(document.getElementById('sentiment-data').textContent);
163
+ const topicData = JSON.parse(document.getElementById('topic-data').textContent);
164
+ const dailyData = JSON.parse(document.getElementById('daily-data').textContent);
165
+
166
+ // Sentiment Chart
167
+ const sentimentLabels = sentimentData.map(item => item.sentiment);
168
+ const sentimentCounts = sentimentData.map(item => item.count);
169
+ new Chart(document.getElementById('sentimentChart'), {
170
+ type: 'doughnut',
171
+ data: {
172
+ labels: sentimentLabels,
173
+ datasets: [{
174
+ data: sentimentCounts,
175
+ backgroundColor: ['#28a745', '#ffc107', '#dc3545']
176
+ }]
177
+ },
178
+ options: {
179
+ responsive: true,
180
+ maintainAspectRatio: false,
181
+ plugins: {
182
+ legend: {
183
+ position: 'bottom',
184
+ labels: {
185
+ generateLabels: function(chart) {
186
+ const data = chart.data;
187
+ if (data.labels.length && data.datasets.length) {
188
+ const dataset = data.datasets[0];
189
+ const total = dataset.data.reduce((a, b) => a + b, 0);
190
+ return data.labels.map((label, i) => {
191
+ const value = dataset.data[i];
192
+ const percentage = ((value / total) * 100).toFixed(1);
193
+ return {
194
+ text: `${label}: ${value} (${percentage}%)`,
195
+ fillStyle: dataset.backgroundColor[i],
196
+ strokeStyle: dataset.backgroundColor[i],
197
+ lineWidth: 1,
198
+ hidden: false,
199
+ index: i
200
+ };
201
+ });
202
+ }
203
+ return [];
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ });
210
+
211
+ // Topic Chart
212
+ const topicLabels = topicData.map(item => item.topic);
213
+ const topicCounts = topicData.map(item => item.count);
214
+ new Chart(document.getElementById('topicChart'), {
215
+ type: 'bar',
216
+ data: {
217
+ labels: topicLabels,
218
+ datasets: [{
219
+ data: topicCounts,
220
+ backgroundColor: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
221
+ }]
222
+ },
223
+ options: {
224
+ responsive: true,
225
+ maintainAspectRatio: false,
226
+ scales: { y: { beginAtZero: true } },
227
+ plugins: {
228
+ legend: {
229
+ display: false
230
+ }
231
+ }
232
+ }
233
+ });
234
+
235
+ // Daily Chart
236
+ const dailyLabels = dailyData.map(item => item.date);
237
+ const dailyCounts = dailyData.map(item => item.count);
238
+ new Chart(document.getElementById('dailyChart'), {
239
+ type: 'line',
240
+ data: {
241
+ labels: dailyLabels,
242
+ datasets: [{
243
+ label: 'Feedback/ngày',
244
+ data: dailyCounts,
245
+ borderColor: '#28a745',
246
+ backgroundColor: 'rgba(40, 167, 69, 0.1)',
247
+ tension: 0.4
248
+ }]
249
+ },
250
+ options: {
251
+ responsive: true,
252
+ maintainAspectRatio: false,
253
+ scales: { y: { beginAtZero: true } }
254
+ }
255
+ });
256
+ });
257
+ </script>
258
+ {% endblock %}
templates/index.html CHANGED
@@ -1,64 +1,11 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Student Feedback Sentiment Analysis</title>
7
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
- <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
10
- </head>
11
- <body>
12
- <div class="container-fluid">
13
- <!-- Header -->
14
- <header>
15
- <div class="d-flex justify-content-between align-items-center">
16
- <h1 class="display-4 fw-bold text-primary mb-0">
17
- <i class="fas fa-graduation-cap me-1" style="margin-left: 10px !important;"></i>
18
- Student Feedback Analysis
19
- </h1>
20
- <div class="d-flex align-items-center justify-content-end" style="margin-right: 20px; gap: 16px;">
21
- <span class="text-white fw-semibold d-flex align-items-center">
22
- <i class="fas fa-user me-2"></i>
23
- {{ current_user.username }}
24
- </span>
25
- <a href="{{ url_for('logout') }}"
26
- style="background-color: rgba(255,255,255,0.2);
27
- border: 1px solid rgba(255,255,255,0.3);
28
- color: white;
29
- padding: 8px 8px;
30
- border-radius: 6px;
31
- text-decoration: none;
32
- font-weight: 600;
33
- display: inline-flex;
34
- align-items: center;
35
- justify-content: center;
36
- font-size: 13px;
37
- transition: all 0.3s ease;
38
- backdrop-filter: blur(10px);
39
- gap: 6px;">
40
- <i class="fas fa-sign-out-alt"></i>
41
- <span>Đăng xuất</span>
42
- </a>
43
- </div>
44
- </div>
45
- </header>
46
 
47
- <!-- Main Content -->
48
- <div class="row justify-content-center">
49
- <div class="col-lg-8">
50
- <!-- Flash Messages -->
51
- {% with messages = get_flashed_messages(with_categories=true) %}
52
- {% if messages %}
53
- {% for category, message in messages %}
54
- <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
55
- <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' }} me-2"></i>
56
- {{ message }}
57
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
58
- </div>
59
- {% endfor %}
60
- {% endif %}
61
- {% endwith %}
62
 
63
  <!-- Input Form -->
64
  <div class="card shadow-lg">
@@ -176,7 +123,7 @@
176
  </div>
177
 
178
  <!-- Feedback History -->
179
- <div class="card shadow-sm mt-4">
180
  <div class="card-header bg-primary text-white">
181
  <h4 class="card-title mb-0">
182
  <i class="fas fa-history me-2"></i>
@@ -201,33 +148,10 @@
201
  </div>
202
  </div>
203
 
204
- <!-- Footer -->
205
- <footer class="text-center py-4 mt-5">
206
- <p class="text-muted mb-0">
207
- <i class="fas fa-robot me-2"></i>
208
- Powered by Tư _ Đức _ Nghĩa _ Hà
209
- </p>
210
- </footer>
211
  </div>
 
 
212
 
213
- <!-- Scripts -->
214
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
215
- <script src="{{ url_for('static', filename='js/app.js') }}"></script>
216
- <script>
217
- // Auto-hide alerts after 3 seconds with smooth fade
218
- document.addEventListener('DOMContentLoaded', function() {
219
- const alerts = document.querySelectorAll('.alert');
220
- alerts.forEach(function(alert) {
221
- setTimeout(function() {
222
- // Add fade-out class for smooth transition
223
- alert.classList.add('fade-out');
224
- // Remove from DOM after transition completes
225
- setTimeout(function() {
226
- alert.remove();
227
- }, 500);
228
- }, 3000);
229
- });
230
- });
231
- </script>
232
- </body>
233
- </html>
 
1
+ {% extends "base.html" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ {% block title %}Student Feedback Sentiment Analysis{% endblock %}
4
+ {% block page_title %}Student Feedback Analysis{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="row justify-content-center">
8
+ <div class="col-lg-8">
 
 
 
 
 
 
 
 
 
9
 
10
  <!-- Input Form -->
11
  <div class="card shadow-lg">
 
123
  </div>
124
 
125
  <!-- Feedback History -->
126
+ <div id="feedbackHistory" class="card shadow-sm mt-4">
127
  <div class="card-header bg-primary text-white">
128
  <h4 class="card-title mb-0">
129
  <i class="fas fa-history me-2"></i>
 
148
  </div>
149
  </div>
150
 
 
 
 
 
 
 
 
151
  </div>
152
+ </div>
153
+ {% endblock %}
154
 
155
+ {% block extra_scripts %}
156
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
157
+ {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/login.html CHANGED
@@ -1,39 +1,39 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Đăng nhập - Student Feedback Analysis</title>
7
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
- <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
10
- </head>
11
- <body>
12
- <div class="container-fluid">
13
- <!-- Main Content -->
14
- <div class="row justify-content-center" style="padding-top: 60px;">
15
- <div class="col-lg-6 col-md-8">
16
- <!-- Login Form -->
17
- <div class="card shadow-sm">
18
- <div class="card-header bg-primary text-white">
19
- <h3 class="card-title mb-0">
20
- <i class="fas fa-sign-in-alt me-2"></i>
21
- Đăng nhập tài khoản
22
- </h3>
23
- </div>
24
- <div class="card-body">
25
- <!-- Flash Messages -->
26
- {% with messages = get_flashed_messages(with_categories=true) %}
27
- {% if messages %}
28
- {% for category, message in messages %}
29
- <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
30
- <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' }} me-2"></i>
31
- {{ message }}
32
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
33
- </div>
34
- {% endfor %}
35
- {% endif %}
36
- {% endwith %}
37
 
38
  <form method="POST">
39
  {{ form.hidden_tag() }}
@@ -80,34 +80,13 @@
80
  </div>
81
  </div>
82
 
83
- <!-- Back to Home -->
84
- <div class="text-center mt-3">
85
- <a href="{{ url_for('home') }}" class="btn btn-outline-secondary">
86
- <i class="fas fa-arrow-left me-2"></i>
87
- Về trang chủ
88
- </a>
89
- </div>
90
- </div>
91
  </div>
92
  </div>
93
-
94
- <!-- Scripts -->
95
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
96
- <script>
97
- // Auto-hide alerts after 3 seconds with smooth fade
98
- document.addEventListener('DOMContentLoaded', function() {
99
- const alerts = document.querySelectorAll('.alert');
100
- alerts.forEach(function(alert) {
101
- setTimeout(function() {
102
- // Add fade-out class for smooth transition
103
- alert.classList.add('fade-out');
104
- // Remove from DOM after transition completes
105
- setTimeout(function() {
106
- alert.remove();
107
- }, 500);
108
- }, 3000);
109
- });
110
- });
111
- </script>
112
- </body>
113
- </html>
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Đăng nhập - Student Feedback Analysis{% endblock %}
4
+ {% block page_title %}Đăng nhập{% endblock %}
5
+ {% block container_class %} min-vh-100{% endblock %}
6
+
7
+ {% block header %}{% endblock %}
8
+ {% block flash_messages %}{% endblock %}
9
+ {% block footer %}{% endblock %}
10
+
11
+ {% block content %}
12
+ <div class="row justify-content-center align-items-center min-vh-100">
13
+ <div class="col-lg-6 col-md-8">
14
+ <!-- Login Form -->
15
+ <div class="card shadow-sm">
16
+ <div class="card-header bg-primary text-white">
17
+ <h3 class="card-title mb-0">
18
+ <i class="fas fa-sign-in-alt me-2"></i>
19
+ Đăng nhập tài khoản
20
+ </h3>
21
+ </div>
22
+ <div class="card-body">
23
+ <!-- Flash Messages -->
24
+ {% with messages = get_flashed_messages(with_categories=true) %}
25
+ {% if messages %}
26
+ {% for category, message in messages %}
27
+ <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
28
+ <div class="alert-content">
29
+ <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' if category == 'info' else 'check-circle' if category == 'success' else 'exclamation-triangle' }}"></i>
30
+ {{ message }}
31
+ </div>
32
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
33
+ </div>
34
+ {% endfor %}
35
+ {% endif %}
36
+ {% endwith %}
37
 
38
  <form method="POST">
39
  {{ form.hidden_tag() }}
 
80
  </div>
81
  </div>
82
 
83
+ <!-- Back to Home -->
84
+ <div class="text-center mt-3">
85
+ <a href="{{ url_for('home') }}" class="btn btn-outline-secondary">
86
+ <i class="fas fa-arrow-left me-2"></i>
87
+ Về trang chủ
88
+ </a>
 
 
89
  </div>
90
  </div>
91
+ </div>
92
+ {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/my_statistics.html ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Thống kê cá nhân - Student Feedback Analysis{% endblock %}
4
+ {% block page_title %}Thống kê feedback của tôi{% endblock %}
5
+
6
+ {% block extra_head %}
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <style>
9
+ /* Use global styles - remove custom database styling */
10
+ .table thead th { color: #374151; }
11
+ .table tbody td { color: #374151; }
12
+ </style>
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <!-- Thống kê tổng quan -->
17
+ <div class="row mb-4">
18
+ <div class="col-md-12">
19
+ <div class="card text-center">
20
+ <div class="card-body">
21
+ <i class="fas fa-comments fa-3x mb-3" style="color: #10B981 !important;"></i>
22
+ <h3 class="card-title">{{ total_feedbacks }}</h3>
23
+ <p class="card-text">Tổng số feedback của tôi</p>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <!-- Biểu đồ thống kê -->
30
+ <div class="row mb-4">
31
+ <div class="col-md-6">
32
+ <div class="card">
33
+ <div class="card-header">
34
+ <h5 class="mb-0"><i class="fas fa-chart-pie me-2" style="color: #F59E0B !important;"></i>Phân bố Sentiment</h5>
35
+ </div>
36
+ <div class="card-body">
37
+ <canvas id="sentimentChart" width="400" height="200"></canvas>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <div class="col-md-6">
42
+ <div class="card">
43
+ <div class="card-header">
44
+ <h5 class="mb-0"><i class="fas fa-chart-bar me-2" style="color: #6B7280 !important;"></i>Phân bố Topic</h5>
45
+ </div>
46
+ <div class="card-body">
47
+ <canvas id="topicChart" width="400" height="200"></canvas>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Feedback gần nhất -->
54
+ <div class="card mb-4">
55
+ <div class="card-header">
56
+ <h5 class="mb-0"><i class="fas fa-history me-2" style="color: #EF4444 !important;"></i>Feedback gần nhất của tôi</h5>
57
+ </div>
58
+ <div class="card-body">
59
+ {% if recent_feedbacks %}
60
+ <div class="table-responsive">
61
+ <table class="table table-striped">
62
+ <thead>
63
+ <tr>
64
+ <th>ID</th>
65
+ <th>Feedback</th>
66
+ <th>Sentiment</th>
67
+ <th>Topic</th>
68
+ <th>Thời gian</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody>
72
+ {% for feedback in recent_feedbacks %}
73
+ <tr>
74
+ <td>{{ feedback.id }}</td>
75
+ <td>
76
+ <div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">
77
+ {{ feedback.text }}
78
+ </div>
79
+ </td>
80
+ <td>
81
+ <span class="badge bg-{{ 'success' if feedback.sentiment == 'positive' else 'warning' if feedback.sentiment == 'neutral' else 'danger' }}">
82
+ {{ feedback.sentiment }}
83
+ </span>
84
+ </td>
85
+ <td>
86
+ <span class="badge bg-secondary">{{ feedback.topic }}</span>
87
+ </td>
88
+ <td>{{ feedback.created_at.strftime('%d/%m/%Y %H:%M') }}</td>
89
+ </tr>
90
+ {% endfor %}
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+ {% else %}
95
+ <p class="text-muted text-center">Chưa có feedback nào.</p>
96
+ {% endif %}
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Thống kê theo ngày -->
101
+ <div class="row">
102
+ <div class="col-md-12">
103
+ <div class="card">
104
+ <div class="card-header">
105
+ <h5 class="mb-0"><i class="fas fa-chart-line me-2" style="color: #06B6D4 !important;"></i>Feedback theo ngày (30 ngày gần nhất)</h5>
106
+ </div>
107
+ <div class="card-body">
108
+ <canvas id="dailyChart" width="400" height="200"></canvas>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Hidden data elements -->
116
+ <div id="sentiment-data" style="display: none;">{{ sentiment_stats | tojson | safe }}</div>
117
+ <div id="topic-data" style="display: none;">{{ topic_stats | tojson | safe }}</div>
118
+ <div id="daily-data" style="display: none;">{{ daily_stats | tojson | safe }}</div>
119
+
120
+ {% endblock %}
121
+
122
+ {% block extra_scripts %}
123
+ <script>
124
+ // Initialize charts when page loads
125
+ document.addEventListener('DOMContentLoaded', function() {
126
+ // Get data from hidden elements
127
+ const sentimentData = JSON.parse(document.getElementById('sentiment-data').textContent);
128
+ const topicData = JSON.parse(document.getElementById('topic-data').textContent);
129
+ const dailyData = JSON.parse(document.getElementById('daily-data').textContent);
130
+
131
+ // Sentiment Chart
132
+ const sentimentLabels = sentimentData.map(item => item.sentiment);
133
+ const sentimentCounts = sentimentData.map(item => item.count);
134
+ new Chart(document.getElementById('sentimentChart'), {
135
+ type: 'doughnut',
136
+ data: {
137
+ labels: sentimentLabels,
138
+ datasets: [{
139
+ data: sentimentCounts,
140
+ backgroundColor: ['#28a745', '#ffc107', '#dc3545']
141
+ }]
142
+ },
143
+ options: {
144
+ responsive: true,
145
+ maintainAspectRatio: false,
146
+ plugins: {
147
+ legend: {
148
+ position: 'bottom',
149
+ labels: {
150
+ generateLabels: function(chart) {
151
+ const data = chart.data;
152
+ if (data.labels.length && data.datasets.length) {
153
+ const dataset = data.datasets[0];
154
+ const total = dataset.data.reduce((a, b) => a + b, 0);
155
+ return data.labels.map((label, i) => {
156
+ const value = dataset.data[i];
157
+ const percentage = ((value / total) * 100).toFixed(1);
158
+ return {
159
+ text: `${label}: ${value} (${percentage}%)`,
160
+ fillStyle: dataset.backgroundColor[i],
161
+ strokeStyle: dataset.backgroundColor[i],
162
+ lineWidth: 1,
163
+ hidden: false,
164
+ index: i
165
+ };
166
+ });
167
+ }
168
+ return [];
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ });
175
+
176
+ // Topic Chart
177
+ const topicLabels = topicData.map(item => item.topic);
178
+ const topicCounts = topicData.map(item => item.count);
179
+ new Chart(document.getElementById('topicChart'), {
180
+ type: 'bar',
181
+ data: {
182
+ labels: topicLabels,
183
+ datasets: [{
184
+ data: topicCounts,
185
+ backgroundColor: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
186
+ }]
187
+ },
188
+ options: {
189
+ responsive: true,
190
+ maintainAspectRatio: false,
191
+ scales: { y: { beginAtZero: true } },
192
+ plugins: {
193
+ legend: {
194
+ display: false
195
+ }
196
+ }
197
+ }
198
+ });
199
+
200
+ // Daily Chart
201
+ const dailyLabels = dailyData.map(item => item.date);
202
+ const dailyCounts = dailyData.map(item => item.count);
203
+ new Chart(document.getElementById('dailyChart'), {
204
+ type: 'line',
205
+ data: {
206
+ labels: dailyLabels,
207
+ datasets: [{
208
+ label: 'Feedback/ngày',
209
+ data: dailyCounts,
210
+ borderColor: '#28a745',
211
+ backgroundColor: 'rgba(40, 167, 69, 0.1)',
212
+ tension: 0.4
213
+ }]
214
+ },
215
+ options: {
216
+ responsive: true,
217
+ maintainAspectRatio: false,
218
+ scales: { y: { beginAtZero: true } }
219
+ }
220
+ });
221
+ });
222
+ </script>
223
+ {% endblock %}
templates/register.html CHANGED
@@ -1,39 +1,39 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Đăng ký - Student Feedback Analysis</title>
7
- <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
- <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
10
- </head>
11
- <body>
12
- <div class="container-fluid">
13
- <!-- Main Content -->
14
- <div class="row justify-content-center" style="padding-top: 60px;">
15
- <div class="col-lg-6 col-md-8">
16
- <!-- Registration Form -->
17
- <div class="card shadow-sm">
18
- <div class="card-header bg-primary text-white">
19
- <h3 class="card-title mb-0">
20
- <i class="fas fa-user-plus me-2"></i>
21
- Tạo tài khoản mới
22
- </h3>
23
- </div>
24
- <div class="card-body">
25
- <!-- Flash Messages -->
26
- {% with messages = get_flashed_messages(with_categories=true) %}
27
- {% if messages %}
28
- {% for category, message in messages %}
29
- <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
30
- <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' }} me-2"></i>
31
- {{ message }}
32
- <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
33
- </div>
34
- {% endfor %}
35
- {% endif %}
36
- {% endwith %}
37
 
38
  <form method="POST">
39
  {{ form.hidden_tag() }}
@@ -92,34 +92,13 @@
92
  </div>
93
  </div>
94
 
95
- <!-- Back to Home -->
96
- <div class="text-center mt-3">
97
- <a href="{{ url_for('home') }}" class="btn btn-outline-secondary">
98
- <i class="fas fa-arrow-left me-2"></i>
99
- Về trang chủ
100
- </a>
101
- </div>
102
- </div>
103
  </div>
104
  </div>
105
-
106
- <!-- Scripts -->
107
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
108
- <script>
109
- // Auto-hide alerts after 3 seconds with smooth fade
110
- document.addEventListener('DOMContentLoaded', function() {
111
- const alerts = document.querySelectorAll('.alert');
112
- alerts.forEach(function(alert) {
113
- setTimeout(function() {
114
- // Add fade-out class for smooth transition
115
- alert.classList.add('fade-out');
116
- // Remove from DOM after transition completes
117
- setTimeout(function() {
118
- alert.remove();
119
- }, 500);
120
- }, 3000);
121
- });
122
- });
123
- </script>
124
- </body>
125
- </html>
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Đăng ký - Student Feedback Analysis{% endblock %}
4
+ {% block page_title %}Đăng ký{% endblock %}
5
+ {% block container_class %} min-vh-100{% endblock %}
6
+
7
+ {% block header %}{% endblock %}
8
+ {% block flash_messages %}{% endblock %}
9
+ {% block footer %}{% endblock %}
10
+
11
+ {% block content %}
12
+ <div class="row justify-content-center align-items-center min-vh-100">
13
+ <div class="col-lg-6 col-md-8">
14
+ <!-- Registration Form -->
15
+ <div class="card shadow-sm">
16
+ <div class="card-header bg-primary text-white">
17
+ <h3 class="card-title mb-0">
18
+ <i class="fas fa-user-plus me-2"></i>
19
+ Tạo tài khoản mới
20
+ </h3>
21
+ </div>
22
+ <div class="card-body">
23
+ <!-- Flash Messages -->
24
+ {% with messages = get_flashed_messages(with_categories=true) %}
25
+ {% if messages %}
26
+ {% for category, message in messages %}
27
+ <div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
28
+ <div class="alert-content">
29
+ <i class="fas fa-{{ 'exclamation-triangle' if category == 'error' else 'info-circle' if category == 'info' else 'check-circle' if category == 'success' else 'exclamation-triangle' }}"></i>
30
+ {{ message }}
31
+ </div>
32
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
33
+ </div>
34
+ {% endfor %}
35
+ {% endif %}
36
+ {% endwith %}
37
 
38
  <form method="POST">
39
  {{ form.hidden_tag() }}
 
92
  </div>
93
  </div>
94
 
95
+ <!-- Back to Home -->
96
+ <div class="text-center mt-3">
97
+ <a href="{{ url_for('home') }}" class="btn btn-outline-secondary">
98
+ <i class="fas fa-arrow-left me-2"></i>
99
+ Về trang chủ
100
+ </a>
 
 
101
  </div>
102
  </div>
103
+ </div>
104
+ {% endblock %}