Upload 6 files
Browse files- PhoBERTMultiTask.py +32 -0
- app.py +78 -0
- requirements.txt +9 -0
- static/css/style.css +289 -0
- static/js/app.js +294 -0
- 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>
|