Ptul2x5 commited on
Commit
663dc0a
·
verified ·
1 Parent(s): 8a928c5

Upload 6 files

Browse files
Files changed (6) hide show
  1. PhoBERTMultiTask.py +32 -0
  2. app.py +78 -0
  3. requirements.txt +9 -0
  4. static/css/style.css +289 -0
  5. static/js/app.js +294 -0
  6. templates/index.html +164 -0
PhoBERTMultiTask.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ from transformers import AutoModel
4
+
5
+ class PhoBERTMultiTask(nn.Module):
6
+ def __init__(self, num_sentiment=3, num_topic=4):
7
+ super().__init__()
8
+ self.phobert = AutoModel.from_pretrained("vinai/phobert-base")
9
+ self.dropout = nn.Dropout(0.1)
10
+
11
+
12
+ self.sentiment_head = nn.Sequential(
13
+ nn.Linear(768, 768),
14
+ nn.ReLU(),
15
+ nn.Dropout(0.1),
16
+ nn.Linear(768, num_sentiment)
17
+ )
18
+
19
+ self.topic_head = nn.Sequential(
20
+ nn.Linear(768, 768),
21
+ nn.ReLU(),
22
+ nn.Dropout(0.1),
23
+ nn.Linear(768, num_topic)
24
+ )
25
+
26
+ def forward(self, input_ids, attention_mask):
27
+ outputs = self.phobert(input_ids=input_ids, attention_mask=attention_mask)
28
+ pooled = outputs.last_hidden_state[:, 0, :] # [CLS] token
29
+ pooled = self.dropout(pooled)
30
+ logits_sent = self.sentiment_head(pooled)
31
+ logits_topic = self.topic_head(pooled)
32
+ return logits_sent, logits_topic
app.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from transformers import AutoTokenizer
4
+ from flask import Flask, request, jsonify, render_template
5
+ from PhoBERTMultiTask import PhoBERTMultiTask
6
+
7
+ app = Flask(__name__)
8
+
9
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
10
+
11
+ # === Load tokenizer & model ===
12
+ MODEL_REPO = "Ptul2x5/Student_Feedback_Sentiment" # 🔹 Repo Hugging Face của bạn
13
+
14
+ print("🔄 Đang tải tokenizer và model từ Hugging Face...")
15
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO, use_fast=False)
16
+
17
+ # 🔹 Tải trọng số model (.bin) trực tiếp từ Hugging Face
18
+ state_dict = torch.hub.load_state_dict_from_url(
19
+ f"https://huggingface.co/{MODEL_REPO}/resolve/main/multitask_model.bin",
20
+ map_location=device
21
+ )
22
+
23
+ model = PhoBERTMultiTask(num_sentiment=3, num_topic=4)
24
+ model.load_state_dict(state_dict)
25
+ model.to(device)
26
+ model.eval()
27
+ print("✅ Model đã sẵn sàng!")
28
+
29
+ # ====== ROUTES ======
30
+ @app.route("/", methods=["GET"])
31
+ def home():
32
+ return render_template('index.html')
33
+
34
+ @app.route("/api/health", methods=["GET"])
35
+ def health():
36
+ return jsonify({"status": "healthy", "message": "✅ PhoBERT MultiTask API is running!"})
37
+
38
+ @app.route("/predict", methods=["POST"])
39
+ def predict():
40
+ try:
41
+ data = request.get_json()
42
+ text = data.get("text", "").strip()
43
+
44
+ if not text:
45
+ return jsonify({"error": "Missing 'text' field"}), 400
46
+
47
+ # Validate input length
48
+ if len(text) > 1000:
49
+ return jsonify({"error": "Text quá dài. Vui lòng nhập tối đa 1000 ký tự."}), 400
50
+
51
+ # Tokenize
52
+ inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True, max_length=128).to(device)
53
+
54
+ # Inference
55
+ with torch.no_grad():
56
+ logits_sent, logits_topic = model(inputs["input_ids"], inputs["attention_mask"])
57
+ sent = torch.argmax(logits_sent, dim=1).item()
58
+ topic = torch.argmax(logits_topic, dim=1).item()
59
+
60
+ # Mapping
61
+ sent_map = {0: "negative", 1: "neutral", 2: "positive"}
62
+ topic_map = {0: "lecturer", 1: "training_program", 2: "facility", 3: "others"}
63
+
64
+ return jsonify({
65
+ "sentiment": sent_map[sent],
66
+ "topic": topic_map[topic],
67
+ "confidence": {
68
+ "sentiment": float(torch.softmax(logits_sent, dim=1).max().item()),
69
+ "topic": float(torch.softmax(logits_topic, dim=1).max().item())
70
+ }
71
+ })
72
+ except Exception as e:
73
+ return jsonify({"error": f"Có lỗi xảy ra khi xử lý: {str(e)}"}), 500
74
+
75
+
76
+ if __name__ == "__main__":
77
+ port = int(os.environ.get("PORT", 10000))
78
+ app.run(host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Flask==3.0.3
2
+ torch==2.3.1
3
+ transformers==4.44.0
4
+ tokenizers==0.19.1
5
+ huggingface-hub>=0.23.2
6
+ tqdm
7
+ gunicorn
8
+ numpy
9
+ requests
static/css/style.css ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for Student Feedback Analysis */
2
+
3
+ :root {
4
+ --primary-color: #84adec;
5
+ --success-color: #198754;
6
+ --info-color: #0dcaf0;
7
+ --warning-color: #ffc107;
8
+ --danger-color: #dc3545;
9
+ --light-color: #f8f9fa;
10
+ --dark-color: #212529;
11
+ }
12
+
13
+ body {
14
+ background: linear-gradient(135deg, #5e84cb 0%, #6fa0f4 100%);
15
+ min-height: 100vh;
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ }
18
+
19
+ .container-fluid {
20
+ background: rgba(255, 255, 255, 0.98);
21
+ backdrop-filter: blur(10px);
22
+ border-radius: 20px;
23
+ margin: 20px;
24
+ padding: 0;
25
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
26
+ }
27
+
28
+ /* Header Styles */
29
+ header {
30
+ background: linear-gradient(135deg, #7c97f0, #3b82f6);
31
+ color: white;
32
+ border-radius: 20px 20px 0 0;
33
+ margin: 0;
34
+ }
35
+
36
+ header h1 {
37
+ color: #ffd700 !important;
38
+ text-shadow: 3px 3px 12px rgba(0, 0, 0, 0.8);
39
+ margin: 0;
40
+ font-weight: 800;
41
+ letter-spacing: 1px;
42
+ }
43
+
44
+ /* Force header text colors with high specificity */
45
+ header .display-4,
46
+ header h1.display-4,
47
+ header .text-primary {
48
+ color: #ffd700 !important;
49
+ }
50
+
51
+ header p,
52
+ header .lead,
53
+ header p.lead {
54
+ color: #ffffff !important;
55
+ text-shadow: 2px 2px 6px rgba(0, 0, 0, 0.7);
56
+ font-weight: 500;
57
+ font-size: 1.2rem;
58
+ }
59
+
60
+ /* Additional specificity for Bootstrap classes */
61
+ .container-fluid header h1,
62
+ .container-fluid header .display-4 {
63
+ color: #ffd700 !important;
64
+ }
65
+
66
+ .container-fluid header p,
67
+ .container-fluid header .lead {
68
+ color: #ffffff !important;
69
+ }
70
+
71
+ /* Card Styles */
72
+ .card {
73
+ border: none;
74
+ border-radius: 15px;
75
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
76
+ }
77
+
78
+ .card:hover {
79
+ transform: translateY(-5px);
80
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15) !important;
81
+ }
82
+
83
+ .card-header {
84
+ border-radius: 15px 15px 0 0 !important;
85
+ border: none;
86
+ padding: 1.5rem;
87
+ }
88
+
89
+ .card-body {
90
+ padding: 2rem;
91
+ }
92
+
93
+ /* Form Styles */
94
+ .form-control {
95
+ border-radius: 10px;
96
+ border: 2px solid #e9ecef;
97
+ padding: 1rem;
98
+ font-size: 1.1rem;
99
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
100
+ background-color: #ffffff;
101
+ color: #212529;
102
+ }
103
+
104
+ .form-control:focus {
105
+ border-color: #1e40af;
106
+ box-shadow: 0 0 0 0.2rem rgba(30, 64, 175, 0.25);
107
+ background-color: #ffffff;
108
+ }
109
+
110
+ .form-label {
111
+ color: #1f2937 !important;
112
+ font-size: 1.1rem;
113
+ font-weight: 600;
114
+ }
115
+
116
+ .form-text {
117
+ color: #6b7280 !important;
118
+ }
119
+
120
+ /* Button Styles */
121
+ .btn {
122
+ border-radius: 10px;
123
+ padding: 0.75rem 2rem;
124
+ font-weight: 600;
125
+ text-transform: uppercase;
126
+ letter-spacing: 0.5px;
127
+ transition: all 0.3s ease;
128
+ }
129
+
130
+ .btn-primary {
131
+ background: linear-gradient(135deg, #1e40af, #3b82f6);
132
+ border: none;
133
+ color: white;
134
+ }
135
+
136
+ .btn-primary:hover {
137
+ background: linear-gradient(135deg, #1e3a8a, #2563eb);
138
+ transform: translateY(-2px);
139
+ box-shadow: 0 10px 20px rgba(30, 64, 175, 0.4);
140
+ color: white;
141
+ }
142
+
143
+ /* Results Styles */
144
+ #results .card-header {
145
+ font-weight: 600;
146
+ }
147
+
148
+ #sentimentIcon i,
149
+ #topicIcon i {
150
+ transition: transform 0.3s ease;
151
+ }
152
+
153
+ #sentimentIcon:hover i,
154
+ #topicIcon:hover i {
155
+ transform: scale(1.1);
156
+ }
157
+
158
+ /* Sentiment Colors */
159
+ .sentiment-positive {
160
+ color: var(--success-color) !important;
161
+ }
162
+
163
+ .sentiment-neutral {
164
+ color: var(--warning-color) !important;
165
+ }
166
+
167
+ .sentiment-negative {
168
+ color: var(--danger-color) !important;
169
+ }
170
+
171
+ /* Topic Colors */
172
+ .topic-lecturer {
173
+ color: var(--primary-color) !important;
174
+ }
175
+
176
+ .topic-training_program {
177
+ color: var(--info-color) !important;
178
+ }
179
+
180
+ .topic-facility {
181
+ color: var(--success-color) !important;
182
+ }
183
+
184
+ .topic-others {
185
+ color: var(--warning-color) !important;
186
+ }
187
+
188
+ /* Loading Spinner */
189
+ .spinner-border {
190
+ width: 3rem;
191
+ height: 3rem;
192
+ }
193
+
194
+ /* Alert Styles */
195
+ .alert {
196
+ border-radius: 10px;
197
+ border: none;
198
+ padding: 1.5rem;
199
+ }
200
+
201
+ /* Blockquote Styles */
202
+ blockquote {
203
+ border-left: 4px solid var(--primary-color);
204
+ padding-left: 1.5rem;
205
+ font-style: italic;
206
+ color: var(--dark-color);
207
+ }
208
+
209
+ /* Footer Styles */
210
+ footer {
211
+ background: var(--light-color);
212
+ border-radius: 0 0 20px 20px;
213
+ margin: 0;
214
+ }
215
+
216
+ /* Responsive Design */
217
+ @media (max-width: 768px) {
218
+ .container-fluid {
219
+ margin: 10px;
220
+ border-radius: 15px;
221
+ }
222
+
223
+ header {
224
+ border-radius: 15px 15px 0 0;
225
+ padding: 2rem 1rem !important;
226
+ }
227
+
228
+ header h1 {
229
+ font-size: 2rem;
230
+ }
231
+
232
+ .card-body {
233
+ padding: 1.5rem;
234
+ }
235
+
236
+ .btn {
237
+ padding: 0.75rem 1.5rem;
238
+ }
239
+ }
240
+
241
+ /* Animation Classes */
242
+ .fade-in {
243
+ animation: fadeIn 0.5s ease-in;
244
+ }
245
+
246
+ @keyframes fadeIn {
247
+ from {
248
+ opacity: 0;
249
+ transform: translateY(20px);
250
+ }
251
+ to {
252
+ opacity: 1;
253
+ transform: translateY(0);
254
+ }
255
+ }
256
+
257
+ .slide-up {
258
+ animation: slideUp 0.5s ease-out;
259
+ }
260
+
261
+ @keyframes slideUp {
262
+ from {
263
+ opacity: 0;
264
+ transform: translateY(30px);
265
+ }
266
+ to {
267
+ opacity: 1;
268
+ transform: translateY(0);
269
+ }
270
+ }
271
+
272
+ /* Custom Scrollbar */
273
+ ::-webkit-scrollbar {
274
+ width: 8px;
275
+ }
276
+
277
+ ::-webkit-scrollbar-track {
278
+ background: #f1f1f1;
279
+ border-radius: 10px;
280
+ }
281
+
282
+ ::-webkit-scrollbar-thumb {
283
+ background: var(--primary-color);
284
+ border-radius: 10px;
285
+ }
286
+
287
+ ::-webkit-scrollbar-thumb:hover {
288
+ background: #0056b3;
289
+ }
static/js/app.js ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Student Feedback Analysis - Frontend JavaScript
2
+
3
+ document.addEventListener('DOMContentLoaded', function() {
4
+ const feedbackForm = document.getElementById('feedbackForm');
5
+ const loadingSpinner = document.getElementById('loadingSpinner');
6
+ const results = document.getElementById('results');
7
+ const errorMessage = document.getElementById('errorMessage');
8
+ const analyzeBtn = document.getElementById('analyzeBtn');
9
+
10
+ // Form submission handler
11
+ feedbackForm.addEventListener('submit', async function(e) {
12
+ e.preventDefault();
13
+
14
+ const feedbackText = document.getElementById('feedbackText').value.trim();
15
+
16
+ if (!feedbackText) {
17
+ showError('Vui lòng nhập feedback trước khi phân tích!');
18
+ return;
19
+ }
20
+
21
+ // Show loading state
22
+ showLoading();
23
+
24
+ try {
25
+ const response = await fetch('/predict', {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify({ text: feedbackText })
31
+ });
32
+
33
+ const data = await response.json();
34
+
35
+ if (response.ok) {
36
+ displayResults(data, feedbackText);
37
+ } else {
38
+ showError(data.error || 'Có lỗi xảy ra khi phân tích feedback!');
39
+ }
40
+ } catch (error) {
41
+ showError('Không thể kết nối đến server. Vui lòng thử lại!');
42
+ console.error('Error:', error);
43
+ } finally {
44
+ hideLoading();
45
+ }
46
+ });
47
+
48
+ // Show loading spinner
49
+ function showLoading() {
50
+ loadingSpinner.style.display = 'block';
51
+ results.style.display = 'none';
52
+ errorMessage.style.display = 'none';
53
+ analyzeBtn.disabled = true;
54
+ analyzeBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Đang phân tích...';
55
+ }
56
+
57
+ // Hide loading spinner
58
+ function hideLoading() {
59
+ loadingSpinner.style.display = 'none';
60
+ analyzeBtn.disabled = false;
61
+ analyzeBtn.innerHTML = '<i class="fas fa-search me-2"></i>Phân Tích Feedback';
62
+ }
63
+
64
+ // Display results
65
+ function displayResults(data, originalText) {
66
+ const { sentiment, topic } = data;
67
+
68
+ // Update sentiment result
69
+ updateSentimentResult(sentiment);
70
+
71
+ // Update topic result
72
+ updateTopicResult(topic);
73
+
74
+ // Show original text
75
+ document.getElementById('originalText').textContent = originalText;
76
+
77
+ // Show results with animation
78
+ results.style.display = 'block';
79
+ results.classList.add('fade-in');
80
+
81
+ // Scroll to results
82
+ results.scrollIntoView({ behavior: 'smooth' });
83
+ }
84
+
85
+ // Update sentiment result
86
+ function updateSentimentResult(sentiment) {
87
+ const sentimentResult = document.getElementById('sentimentResult');
88
+ const sentimentIcon = document.getElementById('sentimentIcon');
89
+ const sentimentDescription = document.getElementById('sentimentDescription');
90
+
91
+ // Remove existing classes
92
+ sentimentResult.className = 'mb-2';
93
+ sentimentIcon.innerHTML = '';
94
+
95
+ const sentimentConfig = getSentimentConfig(sentiment);
96
+
97
+ // Update icon
98
+ sentimentIcon.innerHTML = `<i class="fas ${sentimentConfig.icon} fa-3x ${sentimentConfig.colorClass}"></i>`;
99
+
100
+ // Update text
101
+ sentimentResult.textContent = sentimentConfig.label;
102
+ sentimentResult.className += ` ${sentimentConfig.colorClass}`;
103
+
104
+ // Update description
105
+ sentimentDescription.textContent = sentimentConfig.description;
106
+ }
107
+
108
+ // Update topic result
109
+ function updateTopicResult(topic) {
110
+ const topicResult = document.getElementById('topicResult');
111
+ const topicIcon = document.getElementById('topicIcon');
112
+ const topicDescription = document.getElementById('topicDescription');
113
+
114
+ // Remove existing classes
115
+ topicResult.className = 'mb-2';
116
+ topicIcon.innerHTML = '';
117
+
118
+ const topicConfig = getTopicConfig(topic);
119
+
120
+ // Update icon
121
+ topicIcon.innerHTML = `<i class="fas ${topicConfig.icon} fa-3x ${topicConfig.colorClass}"></i>`;
122
+
123
+ // Update text
124
+ topicResult.textContent = topicConfig.label;
125
+ topicResult.className += ` ${topicConfig.colorClass}`;
126
+
127
+ // Update description
128
+ topicDescription.textContent = topicConfig.description;
129
+ }
130
+
131
+ // Get sentiment configuration
132
+ function getSentimentConfig(sentiment) {
133
+ const configs = {
134
+ 'positive': {
135
+ icon: 'fa-smile',
136
+ label: 'Tích Cực',
137
+ colorClass: 'sentiment-positive',
138
+ description: 'Feedback thể hiện thái độ tích cực và hài lòng'
139
+ },
140
+ 'neutral': {
141
+ icon: 'fa-meh',
142
+ label: 'Trung Tính',
143
+ colorClass: 'sentiment-neutral',
144
+ description: 'Feedback thể hiện thái độ trung lập, không rõ ràng'
145
+ },
146
+ 'negative': {
147
+ icon: 'fa-frown',
148
+ label: 'Tiêu Cực',
149
+ colorClass: 'sentiment-negative',
150
+ description: 'Feedback thể hiện thái độ tiêu cực và không hài lòng'
151
+ }
152
+ };
153
+
154
+ return configs[sentiment] || configs['neutral'];
155
+ }
156
+
157
+ // Get topic configuration
158
+ function getTopicConfig(topic) {
159
+ const configs = {
160
+ 'lecturer': {
161
+ icon: 'fa-user-tie',
162
+ label: 'Giảng Viên',
163
+ colorClass: 'topic-lecturer',
164
+ description: 'Feedback liên quan đến chất lượng giảng dạy của giảng viên'
165
+ },
166
+ 'training_program': {
167
+ icon: 'fa-graduation-cap',
168
+ label: 'Chương Trình Đào Tạo',
169
+ colorClass: 'topic-training_program',
170
+ description: 'Feedback về nội dung và cấu trúc chương trình học'
171
+ },
172
+ 'facility': {
173
+ icon: 'fa-building',
174
+ label: 'Cơ Sở Vật Chất',
175
+ colorClass: 'topic-facility',
176
+ description: 'Feedback về phòng học, thiết bị và cơ sở hạ tầng'
177
+ },
178
+ 'others': {
179
+ icon: 'fa-ellipsis-h',
180
+ label: 'Khác',
181
+ colorClass: 'topic-others',
182
+ description: 'Feedback về các chủ đề khác không thuộc các danh mục trên'
183
+ }
184
+ };
185
+
186
+ return configs[topic] || configs['others'];
187
+ }
188
+
189
+ // Show error message
190
+ function showError(message) {
191
+ document.getElementById('errorText').textContent = message;
192
+ errorMessage.style.display = 'block';
193
+ errorMessage.classList.add('fade-in');
194
+
195
+ // Hide results if showing
196
+ results.style.display = 'none';
197
+
198
+ // Scroll to error
199
+ errorMessage.scrollIntoView({ behavior: 'smooth' });
200
+ }
201
+
202
+ // Clear form function
203
+ function clearForm() {
204
+ document.getElementById('feedbackText').value = '';
205
+ results.style.display = 'none';
206
+ errorMessage.style.display = 'none';
207
+
208
+ // Reset character counter
209
+ const charCount = document.getElementById('charCount');
210
+ if (charCount) {
211
+ charCount.textContent = '0';
212
+ }
213
+
214
+ // Reset counter color
215
+ const charCounter = document.querySelector('.form-text');
216
+ if (charCounter) {
217
+ charCounter.style.color = '#6c757d';
218
+ }
219
+ }
220
+
221
+ // Add clear button functionality
222
+ const clearBtn = document.createElement('button');
223
+ clearBtn.type = 'button';
224
+ clearBtn.className = 'btn btn-outline-secondary mt-2';
225
+ clearBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Xóa Form';
226
+ clearBtn.onclick = clearForm;
227
+
228
+ feedbackForm.appendChild(clearBtn);
229
+
230
+ // Add example feedbacks
231
+ const exampleBtn = document.createElement('button');
232
+ exampleBtn.type = 'button';
233
+ exampleBtn.className = 'btn btn-outline-info mt-2 ms-2';
234
+ exampleBtn.innerHTML = '<i class="fas fa-lightbulb me-2"></i>Ví Dụ';
235
+ exampleBtn.onclick = showExamples;
236
+
237
+ feedbackForm.appendChild(exampleBtn);
238
+
239
+ // Show example feedbacks
240
+ function showExamples() {
241
+ const examples = [
242
+ "Giảng viên giảng bài rất hay và dễ hiểu, sinh viên rất thích",
243
+ "Phòng học quá nóng và không có điều hòa, rất khó chịu",
244
+ "Chương trình học quá khó và không phù hợp với sinh viên",
245
+ "Cơ sở vật chất rất hiện đại và đầy đủ tiện nghi",
246
+ "Thầy cô rất nhiệt tình và hỗ trợ sinh viên học tập tốt"
247
+ ];
248
+
249
+ const randomExample = examples[Math.floor(Math.random() * examples.length)];
250
+ document.getElementById('feedbackText').value = randomExample;
251
+
252
+ // Trigger input event to update character counter
253
+ const textarea = document.getElementById('feedbackText');
254
+ textarea.dispatchEvent(new Event('input'));
255
+
256
+ // Scroll to form
257
+ feedbackForm.scrollIntoView({ behavior: 'smooth' });
258
+ }
259
+
260
+ // Add keyboard shortcuts
261
+ document.addEventListener('keydown', function(e) {
262
+ // Ctrl + Enter to submit form
263
+ if (e.ctrlKey && e.key === 'Enter') {
264
+ feedbackForm.dispatchEvent(new Event('submit'));
265
+ }
266
+
267
+ // Escape to clear form
268
+ if (e.key === 'Escape') {
269
+ clearForm();
270
+ }
271
+ });
272
+
273
+ // Add character counter
274
+ const textarea = document.getElementById('feedbackText');
275
+ const charCounter = document.createElement('small');
276
+ charCounter.className = 'form-text text-muted';
277
+ charCounter.innerHTML = '<i class="fas fa-info-circle me-1"></i>Độ dài: <span id="charCount">0</span> ký tự';
278
+
279
+ textarea.parentNode.appendChild(charCounter);
280
+
281
+ textarea.addEventListener('input', function() {
282
+ const count = this.value.length;
283
+ document.getElementById('charCount').textContent = count;
284
+
285
+ // Change color based on length
286
+ if (count > 500) {
287
+ charCounter.style.color = '#dc3545';
288
+ } else if (count > 300) {
289
+ charCounter.style.color = '#ffc107';
290
+ } else {
291
+ charCounter.style.color = '#6c757d';
292
+ }
293
+ });
294
+ });
templates/index.html ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/bootstrap@5.1.3/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 class="text-center py-4 mb-4">
15
+ <div class="row">
16
+ <div class="col-12">
17
+ <h1 class="display-4 fw-bold text-primary">
18
+ <i class="fas fa-graduation-cap me-3"></i>
19
+ Student Feedback Analysis
20
+ </h1>
21
+ <p class="lead text-muted">Phân tích sentiment và topic cho feedback của sinh viên</p>
22
+ </div>
23
+ </div>
24
+ </header>
25
+
26
+ <!-- Main Content -->
27
+ <div class="row justify-content-center">
28
+ <div class="col-lg-8">
29
+ <!-- Input Form -->
30
+ <div class="card shadow-lg mb-4">
31
+ <div class="card-header bg-primary text-white">
32
+ <h3 class="card-title mb-0">
33
+ <i class="fas fa-comment-dots me-2"></i>
34
+ Nhập Feedback
35
+ </h3>
36
+ </div>
37
+ <div class="card-body">
38
+ <form id="feedbackForm">
39
+ <div class="mb-3">
40
+ <label for="feedbackText" class="form-label fw-bold">
41
+ <i class="fas fa-edit me-2"></i>
42
+ Feedback của bạn:
43
+ </label>
44
+ <textarea
45
+ class="form-control"
46
+ id="feedbackText"
47
+ rows="4"
48
+ placeholder="Nhập feedback về giảng viên, chương trình đào tạo, cơ sở vật chất..."
49
+ required
50
+ ></textarea>
51
+ <div class="form-text">
52
+ <i class="fas fa-info-circle me-1"></i>
53
+ Nhập feedback bằng tiếng Việt để có kết quả chính xác nhất
54
+ </div>
55
+ </div>
56
+ <div class="d-grid">
57
+ <button type="submit" class="btn btn-primary btn-lg" id="analyzeBtn">
58
+ <i class="fas fa-search me-2"></i>
59
+ Phân Tích Feedback
60
+ </button>
61
+ </div>
62
+ </form>
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Loading Spinner -->
67
+ <div id="loadingSpinner" class="text-center mb-4" style="display: none;">
68
+ <div class="spinner-border text-primary" role="status">
69
+ <span class="visually-hidden">Loading...</span>
70
+ </div>
71
+ <p class="mt-2 text-muted">Đang phân tích feedback...</p>
72
+ </div>
73
+
74
+ <!-- Results -->
75
+ <div id="results" style="display: none;">
76
+ <!-- Sentiment Result -->
77
+ <div class="card shadow-lg mb-3">
78
+ <div class="card-header bg-success text-white">
79
+ <h4 class="card-title mb-0">
80
+ <i class="fas fa-heart me-2"></i>
81
+ Phân loại cảm xúc của feedback
82
+ </h4>
83
+ </div>
84
+ <div class="card-body">
85
+ <div class="row align-items-center">
86
+ <div class="col-md-3">
87
+ <div id="sentimentIcon" class="text-center">
88
+ <i class="fas fa-smile fa-3x"></i>
89
+ </div>
90
+ </div>
91
+ <div class="col-md-9">
92
+ <h3 id="sentimentResult" class="mb-2"></h3>
93
+ <p id="sentimentDescription" class="text-muted mb-0"></p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <!-- Topic Result -->
100
+ <div class="card shadow-lg mb-4">
101
+ <div class="card-header bg-info text-white">
102
+ <h4 class="card-title mb-0">
103
+ <i class="fas fa-tags me-2"></i>
104
+ Chủ đề liên quan đến nội dung feedback
105
+ </h4>
106
+ </div>
107
+ <div class="card-body">
108
+ <div class="row align-items-center">
109
+ <div class="col-md-3">
110
+ <div id="topicIcon" class="text-center">
111
+ <i class="fas fa-user-tie fa-3x"></i>
112
+ </div>
113
+ </div>
114
+ <div class="col-md-9">
115
+ <h3 id="topicResult" class="mb-2"></h3>
116
+ <p id="topicDescription" class="text-muted mb-0"></p>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Original Text -->
123
+ <div class="card shadow-sm">
124
+ <div class="card-header bg-light">
125
+ <h5 class="card-title mb-0">
126
+ <i class="fas fa-quote-left me-2"></i>
127
+ Feedback Gốc
128
+ </h5>
129
+ </div>
130
+ <div class="card-body">
131
+ <blockquote class="blockquote mb-0">
132
+ <p id="originalText" class="mb-0"></p>
133
+ </blockquote>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Error Message -->
139
+ <div id="errorMessage" class="alert alert-danger" style="display: none;">
140
+ <i class="fas fa-exclamation-triangle me-2"></i>
141
+ <span id="errorText"></span>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Footer -->
147
+ <footer class="text-center py-4 mt-5">
148
+ <div class="row">
149
+ <div class="col-12">
150
+ <p class="text-muted mb-0">
151
+ <i class="fas fa-robot me-2"></i>
152
+ Powered by Tư _ Đức _ Nghĩa _ Hà
153
+ </p>
154
+ </div>
155
+ </div>
156
+ </footer>
157
+ </div>
158
+
159
+ <!-- Bootstrap JS -->
160
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
161
+ <!-- Custom JS -->
162
+ <script src="{{ url_for('static', filename='js/app.js') }}"></script>
163
+ </body>
164
+ </html>