Update app.py
Browse files
app.py
CHANGED
@@ -49,7 +49,7 @@ def classify_comment(comment):
|
|
49 |
<div style="font-size: 24px; font-weight: bold; color: {label_info['color']}; margin-bottom: 5px;">
|
50 |
{label_info['name']}
|
51 |
</div>
|
52 |
-
<div style="font-size: 18px; color:
|
53 |
Độ tin cậy: <strong>{round(main_score*100, 1)}%</strong>
|
54 |
</div>
|
55 |
</div>
|
@@ -57,7 +57,7 @@ def classify_comment(comment):
|
|
57 |
else:
|
58 |
main_result = f"Không xác định được nhãn: {main_label}"
|
59 |
|
60 |
-
# Tạo biểu đồ phân phối điểm số
|
61 |
if all_scores:
|
62 |
labels = []
|
63 |
scores = []
|
@@ -70,7 +70,7 @@ def classify_comment(comment):
|
|
70 |
scores.append(item['score'])
|
71 |
colors.append(label_map[label_key]['color'])
|
72 |
|
73 |
-
# Tạo biểu đồ thanh ngang
|
74 |
fig = go.Figure(data=[
|
75 |
go.Bar(
|
76 |
y=labels,
|
@@ -79,7 +79,7 @@ def classify_comment(comment):
|
|
79 |
marker_color=colors,
|
80 |
text=[f"{s:.1%}" for s in scores],
|
81 |
textposition='inside',
|
82 |
-
textfont=dict(color='white', size=12, family='
|
83 |
)
|
84 |
])
|
85 |
|
@@ -87,7 +87,7 @@ def classify_comment(comment):
|
|
87 |
title={
|
88 |
'text': 'Phân phối điểm số dự đoán',
|
89 |
'x': 0.5,
|
90 |
-
'font': {'size': 16, 'family': '
|
91 |
},
|
92 |
xaxis_title="Điểm số",
|
93 |
yaxis_title="Loại bình luận",
|
@@ -95,21 +95,23 @@ def classify_comment(comment):
|
|
95 |
margin=dict(l=20, r=20, t=50, b=20),
|
96 |
plot_bgcolor='rgba(0,0,0,0)',
|
97 |
paper_bgcolor='rgba(0,0,0,0)',
|
98 |
-
font=dict(family="
|
99 |
xaxis=dict(
|
100 |
showgrid=True,
|
101 |
gridwidth=1,
|
102 |
-
gridcolor='
|
103 |
-
range=[0, 1]
|
|
|
104 |
),
|
105 |
yaxis=dict(
|
106 |
-
showgrid=False
|
|
|
107 |
)
|
108 |
)
|
109 |
|
110 |
-
# Chi tiết điểm số
|
111 |
details = "<div style='margin-top: 15px;'>"
|
112 |
-
details += "<h4 style='color:
|
113 |
for item in sorted(all_scores, key=lambda x: x['score'], reverse=True):
|
114 |
label_key = item['label']
|
115 |
if label_key in label_map:
|
@@ -119,10 +121,10 @@ def classify_comment(comment):
|
|
119 |
details += f"""
|
120 |
<div style="margin-bottom: 8px;">
|
121 |
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;">
|
122 |
-
<span style="font-weight: 500;">{info['emoji']} {info['name']}</span>
|
123 |
<span style="font-weight: bold; color: {info['color']};">{percentage:.1f}%</span>
|
124 |
</div>
|
125 |
-
<div style="background:
|
126 |
<div style="background: {info['color']}; height: 100%; width: {bar_width}%; border-radius: 10px;"></div>
|
127 |
</div>
|
128 |
</div>
|
@@ -133,145 +135,476 @@ def classify_comment(comment):
|
|
133 |
|
134 |
return main_result, None, None
|
135 |
|
136 |
-
# Custom CSS
|
137 |
custom_css = """
|
138 |
/* Import Google Fonts */
|
139 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
/* Global styles */
|
142 |
* {
|
143 |
-
font-family: 'Inter', sans-serif !important;
|
|
|
144 |
}
|
145 |
|
146 |
-
/*
|
147 |
.gradio-container {
|
148 |
-
background:
|
149 |
min-height: 100vh;
|
|
|
150 |
}
|
151 |
|
152 |
-
/*
|
153 |
-
|
154 |
-
background:
|
155 |
-
border
|
156 |
-
|
157 |
-
|
158 |
-
overflow: hidden;
|
159 |
}
|
160 |
|
161 |
-
/*
|
162 |
.app-title {
|
163 |
-
background:
|
164 |
-
color:
|
165 |
-
padding: 30px;
|
166 |
text-align: center;
|
|
|
167 |
margin: -20px -20px 30px -20px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
168 |
}
|
169 |
|
170 |
.app-title h1 {
|
171 |
-
font-size: 2.5rem;
|
172 |
font-weight: 700;
|
173 |
margin-bottom: 10px;
|
174 |
-
text-shadow: 2px 2px 4px rgba(0,0,0,0.
|
|
|
|
|
175 |
}
|
176 |
|
177 |
.app-title p {
|
178 |
-
font-size: 1.1rem;
|
179 |
-
opacity: 0.
|
180 |
-
font-weight:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
}
|
182 |
|
183 |
/* Input styling */
|
184 |
-
.input-container textarea
|
185 |
-
|
|
|
|
|
186 |
border-radius: 12px !important;
|
187 |
-
padding:
|
188 |
font-size: 16px !important;
|
|
|
189 |
transition: all 0.3s ease !important;
|
190 |
resize: vertical !important;
|
|
|
191 |
}
|
192 |
|
193 |
-
.input-container textarea:focus
|
|
|
194 |
border-color: #667eea !important;
|
195 |
-
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.
|
196 |
outline: none !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
}
|
198 |
|
199 |
/* Button styling */
|
200 |
-
button.primary
|
201 |
-
|
|
|
202 |
border: none !important;
|
203 |
border-radius: 12px !important;
|
204 |
-
padding:
|
205 |
font-weight: 600 !important;
|
206 |
-
|
207 |
-
|
|
|
|
|
208 |
transition: all 0.3s ease !important;
|
209 |
-
box-shadow:
|
|
|
210 |
}
|
211 |
|
212 |
-
button.primary:hover
|
|
|
213 |
transform: translateY(-2px) !important;
|
214 |
-
box-shadow:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
}
|
216 |
|
217 |
/* Examples styling */
|
218 |
.examples-container {
|
219 |
-
background:
|
|
|
220 |
border-radius: 12px;
|
221 |
padding: 20px;
|
222 |
margin-top: 20px;
|
223 |
}
|
224 |
|
225 |
.examples-container h3 {
|
226 |
-
color:
|
227 |
margin-bottom: 15px;
|
228 |
font-weight: 600;
|
|
|
229 |
}
|
230 |
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
237 |
}
|
238 |
|
239 |
/* Tab styling */
|
240 |
-
.tab-nav button
|
|
|
241 |
border-radius: 8px 8px 0 0 !important;
|
242 |
font-weight: 500 !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
}
|
244 |
|
245 |
-
.tab-nav button
|
246 |
-
|
247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
248 |
}
|
249 |
|
250 |
/* Footer */
|
251 |
.footer {
|
252 |
text-align: center;
|
253 |
-
padding: 20px;
|
254 |
-
color:
|
255 |
font-size: 14px;
|
256 |
-
border-top: 1px solid
|
257 |
-
margin-top:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
258 |
}
|
|
|
259 |
"""
|
260 |
|
261 |
# Tạo giao diện Gradio
|
262 |
with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt - PhoBERT", theme=gr.themes.Soft()) as demo:
|
|
|
|
|
|
|
263 |
# Header
|
264 |
gr.HTML("""
|
265 |
<div class="app-title">
|
266 |
<h1>🧠 Phân loại bình luận tiếng Việt</h1>
|
267 |
-
<p>Sử dụng mô hình PhoBERT để phân tích cảm xúc và độc hại trong bình luận mạng xã hội</p>
|
268 |
</div>
|
269 |
""")
|
270 |
|
271 |
with gr.Row():
|
272 |
with gr.Column(scale=1):
|
273 |
# Input section
|
274 |
-
gr.HTML("<h3
|
275 |
|
276 |
input_text = gr.Textbox(
|
277 |
lines=4,
|
@@ -289,8 +622,8 @@ with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt -
|
|
289 |
# Examples section
|
290 |
gr.HTML("""
|
291 |
<div class="examples-container">
|
292 |
-
<h3>💡 Ví dụ mẫu
|
293 |
-
<p
|
294 |
</div>
|
295 |
""")
|
296 |
|
@@ -310,12 +643,12 @@ with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt -
|
|
310 |
|
311 |
with gr.Column(scale=1):
|
312 |
# Output section
|
313 |
-
gr.HTML("<h3
|
314 |
|
315 |
with gr.Tabs():
|
316 |
with gr.TabItem("🎯 Kết quả chính", elem_id="main_result_tab"):
|
317 |
result_output = gr.HTML(
|
318 |
-
value="<div style='text-align: center; padding: 40px; color:
|
319 |
)
|
320 |
|
321 |
with gr.TabItem("📈 Biểu đồ phân phối", elem_id="chart_tab"):
|
@@ -343,7 +676,7 @@ with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt -
|
|
343 |
<p>
|
344 |
<strong>Mô hình:</strong> PhoBERT fine-tuned cho phân loại bình luận tiếng Việt<br>
|
345 |
<strong>Các nhãn:</strong> Tích cực • Tiêu cực • Trung tính • Độc hại<br>
|
346 |
-
<em>Được xây
|
347 |
</p>
|
348 |
</div>
|
349 |
""")
|
|
|
49 |
<div style="font-size: 24px; font-weight: bold; color: {label_info['color']}; margin-bottom: 5px;">
|
50 |
{label_info['name']}
|
51 |
</div>
|
52 |
+
<div style="font-size: 18px; color: var(--text-secondary);">
|
53 |
Độ tin cậy: <strong>{round(main_score*100, 1)}%</strong>
|
54 |
</div>
|
55 |
</div>
|
|
|
57 |
else:
|
58 |
main_result = f"Không xác định được nhãn: {main_label}"
|
59 |
|
60 |
+
# Tạo biểu đồ phân phối điểm số với theme tự động
|
61 |
if all_scores:
|
62 |
labels = []
|
63 |
scores = []
|
|
|
70 |
scores.append(item['score'])
|
71 |
colors.append(label_map[label_key]['color'])
|
72 |
|
73 |
+
# Tạo biểu đồ thanh ngang với theme responsive
|
74 |
fig = go.Figure(data=[
|
75 |
go.Bar(
|
76 |
y=labels,
|
|
|
79 |
marker_color=colors,
|
80 |
text=[f"{s:.1%}" for s in scores],
|
81 |
textposition='inside',
|
82 |
+
textfont=dict(color='white', size=12, family='Inter')
|
83 |
)
|
84 |
])
|
85 |
|
|
|
87 |
title={
|
88 |
'text': 'Phân phối điểm số dự đoán',
|
89 |
'x': 0.5,
|
90 |
+
'font': {'size': 16, 'family': 'Inter', 'color': 'var(--text-primary)'}
|
91 |
},
|
92 |
xaxis_title="Điểm số",
|
93 |
yaxis_title="Loại bình luận",
|
|
|
95 |
margin=dict(l=20, r=20, t=50, b=20),
|
96 |
plot_bgcolor='rgba(0,0,0,0)',
|
97 |
paper_bgcolor='rgba(0,0,0,0)',
|
98 |
+
font=dict(family="Inter", size=12, color='var(--text-primary)'),
|
99 |
xaxis=dict(
|
100 |
showgrid=True,
|
101 |
gridwidth=1,
|
102 |
+
gridcolor='var(--border-light)',
|
103 |
+
range=[0, 1],
|
104 |
+
color='var(--text-primary)'
|
105 |
),
|
106 |
yaxis=dict(
|
107 |
+
showgrid=False,
|
108 |
+
color='var(--text-primary)'
|
109 |
)
|
110 |
)
|
111 |
|
112 |
+
# Chi tiết điểm số với theme responsive
|
113 |
details = "<div style='margin-top: 15px;'>"
|
114 |
+
details += "<h4 style='color: var(--text-primary); margin-bottom: 10px;'>📊 Chi tiết điểm số:</h4>"
|
115 |
for item in sorted(all_scores, key=lambda x: x['score'], reverse=True):
|
116 |
label_key = item['label']
|
117 |
if label_key in label_map:
|
|
|
121 |
details += f"""
|
122 |
<div style="margin-bottom: 8px;">
|
123 |
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px;">
|
124 |
+
<span style="font-weight: 500; color: var(--text-primary);">{info['emoji']} {info['name']}</span>
|
125 |
<span style="font-weight: bold; color: {info['color']};">{percentage:.1f}%</span>
|
126 |
</div>
|
127 |
+
<div style="background: var(--progress-bg); border-radius: 10px; height: 8px; overflow: hidden;">
|
128 |
<div style="background: {info['color']}; height: 100%; width: {bar_width}%; border-radius: 10px;"></div>
|
129 |
</div>
|
130 |
</div>
|
|
|
135 |
|
136 |
return main_result, None, None
|
137 |
|
138 |
+
# Custom CSS với hỗ trợ light/dark mode
|
139 |
custom_css = """
|
140 |
/* Import Google Fonts */
|
141 |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
142 |
|
143 |
+
/* CSS Variables cho Light Mode */
|
144 |
+
:root {
|
145 |
+
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
146 |
+
--secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
147 |
+
|
148 |
+
/* Light mode colors */
|
149 |
+
--bg-primary: #ffffff;
|
150 |
+
--bg-secondary: #f8fafc;
|
151 |
+
--bg-tertiary: #f1f5f9;
|
152 |
+
--bg-header: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
153 |
+
|
154 |
+
--text-primary: #1e293b;
|
155 |
+
--text-secondary: #64748b;
|
156 |
+
--text-accent: #475569;
|
157 |
+
--text-on-primary: #ffffff;
|
158 |
+
|
159 |
+
--border-light: #e2e8f0;
|
160 |
+
--border-medium: #cbd5e1;
|
161 |
+
--border-strong: #94a3b8;
|
162 |
+
|
163 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
164 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
165 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
166 |
+
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
167 |
+
|
168 |
+
--progress-bg: #e2e8f0;
|
169 |
+
--input-bg: #ffffff;
|
170 |
+
--card-bg: #ffffff;
|
171 |
+
}
|
172 |
+
|
173 |
+
/* Dark Mode */
|
174 |
+
@media (prefers-color-scheme: dark) {
|
175 |
+
:root {
|
176 |
+
--bg-primary: #0f172a;
|
177 |
+
--bg-secondary: #1e293b;
|
178 |
+
--bg-tertiary: #334155;
|
179 |
+
--bg-header: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
|
180 |
+
|
181 |
+
--text-primary: #f1f5f9;
|
182 |
+
--text-secondary: #94a3b8;
|
183 |
+
--text-accent: #cbd5e1;
|
184 |
+
--text-on-primary: #ffffff;
|
185 |
+
|
186 |
+
--border-light: #334155;
|
187 |
+
--border-medium: #475569;
|
188 |
+
--border-strong: #64748b;
|
189 |
+
|
190 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
191 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
192 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
193 |
+
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
|
194 |
+
|
195 |
+
--progress-bg: #334155;
|
196 |
+
--input-bg: #1e293b;
|
197 |
+
--card-bg: #1e293b;
|
198 |
+
}
|
199 |
+
}
|
200 |
+
|
201 |
+
/* Manual dark mode class override */
|
202 |
+
.dark {
|
203 |
+
--bg-primary: #0f172a;
|
204 |
+
--bg-secondary: #1e293b;
|
205 |
+
--bg-tertiary: #334155;
|
206 |
+
--bg-header: linear-gradient(135deg, #4338ca 0%, #7c3aed 100%);
|
207 |
+
|
208 |
+
--text-primary: #f1f5f9;
|
209 |
+
--text-secondary: #94a3b8;
|
210 |
+
--text-accent: #cbd5e1;
|
211 |
+
--text-on-primary: #ffffff;
|
212 |
+
|
213 |
+
--border-light: #334155;
|
214 |
+
--border-medium: #475569;
|
215 |
+
--border-strong: #64748b;
|
216 |
+
|
217 |
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
218 |
+
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
|
219 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
220 |
+
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6);
|
221 |
+
|
222 |
+
--progress-bg: #334155;
|
223 |
+
--input-bg: #1e293b;
|
224 |
+
--card-bg: #1e293b;
|
225 |
+
}
|
226 |
+
|
227 |
/* Global styles */
|
228 |
* {
|
229 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
230 |
+
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important;
|
231 |
}
|
232 |
|
233 |
+
/* Main container styling */
|
234 |
.gradio-container {
|
235 |
+
background: var(--bg-primary) !important;
|
236 |
min-height: 100vh;
|
237 |
+
color: var(--text-primary) !important;
|
238 |
}
|
239 |
|
240 |
+
/* Block container */
|
241 |
+
.block {
|
242 |
+
background: var(--card-bg) !important;
|
243 |
+
border: 1px solid var(--border-light) !important;
|
244 |
+
border-radius: 16px !important;
|
245 |
+
box-shadow: var(--shadow-md) !important;
|
|
|
246 |
}
|
247 |
|
248 |
+
/* Header styling */
|
249 |
.app-title {
|
250 |
+
background: var(--bg-header);
|
251 |
+
color: var(--text-on-primary);
|
252 |
+
padding: 40px 30px;
|
253 |
text-align: center;
|
254 |
+
border-radius: 20px 20px 0 0;
|
255 |
margin: -20px -20px 30px -20px;
|
256 |
+
position: relative;
|
257 |
+
overflow: hidden;
|
258 |
+
}
|
259 |
+
|
260 |
+
.app-title::before {
|
261 |
+
content: '';
|
262 |
+
position: absolute;
|
263 |
+
top: 0;
|
264 |
+
left: 0;
|
265 |
+
right: 0;
|
266 |
+
bottom: 0;
|
267 |
+
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20"><defs><radialGradient id="a" cx="50%" cy="0%" r="100%"><stop offset="0%" stop-color="white" stop-opacity="0.1"/><stop offset="100%" stop-color="white" stop-opacity="0"/></radialGradient></defs><rect width="100" height="20" fill="url(%23a)"/></svg>');
|
268 |
+
pointer-events: none;
|
269 |
}
|
270 |
|
271 |
.app-title h1 {
|
272 |
+
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
273 |
font-weight: 700;
|
274 |
margin-bottom: 10px;
|
275 |
+
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
276 |
+
position: relative;
|
277 |
+
z-index: 1;
|
278 |
}
|
279 |
|
280 |
.app-title p {
|
281 |
+
font-size: clamp(1rem, 2.5vw, 1.1rem);
|
282 |
+
opacity: 0.95;
|
283 |
+
font-weight: 400;
|
284 |
+
position: relative;
|
285 |
+
z-index: 1;
|
286 |
+
max-width: 600px;
|
287 |
+
margin: 0 auto;
|
288 |
+
line-height: 1.6;
|
289 |
+
}
|
290 |
+
|
291 |
+
/* Section headers */
|
292 |
+
h3 {
|
293 |
+
color: var(--text-primary) !important;
|
294 |
+
font-weight: 600 !important;
|
295 |
+
margin-bottom: 15px !important;
|
296 |
+
font-size: 1.25rem !important;
|
297 |
}
|
298 |
|
299 |
/* Input styling */
|
300 |
+
.input-container textarea,
|
301 |
+
textarea {
|
302 |
+
background: var(--input-bg) !important;
|
303 |
+
border: 2px solid var(--border-light) !important;
|
304 |
border-radius: 12px !important;
|
305 |
+
padding: 16px !important;
|
306 |
font-size: 16px !important;
|
307 |
+
color: var(--text-primary) !important;
|
308 |
transition: all 0.3s ease !important;
|
309 |
resize: vertical !important;
|
310 |
+
line-height: 1.5 !important;
|
311 |
}
|
312 |
|
313 |
+
.input-container textarea:focus,
|
314 |
+
textarea:focus {
|
315 |
border-color: #667eea !important;
|
316 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15) !important;
|
317 |
outline: none !important;
|
318 |
+
background: var(--input-bg) !important;
|
319 |
+
}
|
320 |
+
|
321 |
+
.input-container textarea::placeholder,
|
322 |
+
textarea::placeholder {
|
323 |
+
color: var(--text-secondary) !important;
|
324 |
+
opacity: 0.8 !important;
|
325 |
}
|
326 |
|
327 |
/* Button styling */
|
328 |
+
button.primary,
|
329 |
+
.gr-button-primary {
|
330 |
+
background: var(--primary-gradient) !important;
|
331 |
border: none !important;
|
332 |
border-radius: 12px !important;
|
333 |
+
padding: 14px 32px !important;
|
334 |
font-weight: 600 !important;
|
335 |
+
font-size: 16px !important;
|
336 |
+
color: var(--text-on-primary) !important;
|
337 |
+
text-transform: none !important;
|
338 |
+
letter-spacing: 0.025em !important;
|
339 |
transition: all 0.3s ease !important;
|
340 |
+
box-shadow: var(--shadow-md) !important;
|
341 |
+
cursor: pointer !important;
|
342 |
}
|
343 |
|
344 |
+
button.primary:hover,
|
345 |
+
.gr-button-primary:hover {
|
346 |
transform: translateY(-2px) !important;
|
347 |
+
box-shadow: var(--shadow-lg) !important;
|
348 |
+
filter: brightness(1.05) !important;
|
349 |
+
}
|
350 |
+
|
351 |
+
button.primary:active,
|
352 |
+
.gr-button-primary:active {
|
353 |
+
transform: translateY(0) !important;
|
354 |
+
box-shadow: var(--shadow-md) !important;
|
355 |
}
|
356 |
|
357 |
/* Examples styling */
|
358 |
.examples-container {
|
359 |
+
background: var(--bg-secondary);
|
360 |
+
border: 1px solid var(--border-light);
|
361 |
border-radius: 12px;
|
362 |
padding: 20px;
|
363 |
margin-top: 20px;
|
364 |
}
|
365 |
|
366 |
.examples-container h3 {
|
367 |
+
color: var(--text-primary);
|
368 |
margin-bottom: 15px;
|
369 |
font-weight: 600;
|
370 |
+
font-size: 1.1rem;
|
371 |
}
|
372 |
|
373 |
+
.examples-container p {
|
374 |
+
color: var(--text-secondary);
|
375 |
+
margin-bottom: 15px;
|
376 |
+
line-height: 1.5;
|
377 |
+
}
|
378 |
+
|
379 |
+
/* Example buttons */
|
380 |
+
.gr-examples .gr-button {
|
381 |
+
background: var(--bg-tertiary) !important;
|
382 |
+
border: 1px solid var(--border-light) !important;
|
383 |
+
border-radius: 8px !important;
|
384 |
+
color: var(--text-primary) !important;
|
385 |
+
padding: 12px 16px !important;
|
386 |
+
margin: 4px !important;
|
387 |
+
font-size: 14px !important;
|
388 |
+
line-height: 1.4 !important;
|
389 |
+
transition: all 0.2s ease !important;
|
390 |
+
text-align: left !important;
|
391 |
+
}
|
392 |
+
|
393 |
+
.gr-examples .gr-button:hover {
|
394 |
+
background: var(--bg-secondary) !important;
|
395 |
+
border-color: var(--border-medium) !important;
|
396 |
+
transform: translateY(-1px) !important;
|
397 |
+
box-shadow: var(--shadow-sm) !important;
|
398 |
}
|
399 |
|
400 |
/* Tab styling */
|
401 |
+
.tab-nav button,
|
402 |
+
.gr-tab-nav button {
|
403 |
border-radius: 8px 8px 0 0 !important;
|
404 |
font-weight: 500 !important;
|
405 |
+
background: var(--bg-tertiary) !important;
|
406 |
+
color: var(--text-secondary) !important;
|
407 |
+
border: 1px solid var(--border-light) !important;
|
408 |
+
border-bottom: none !important;
|
409 |
+
padding: 12px 20px !important;
|
410 |
+
transition: all 0.2s ease !important;
|
411 |
}
|
412 |
|
413 |
+
.tab-nav button:hover,
|
414 |
+
.gr-tab-nav button:hover {
|
415 |
+
background: var(--bg-secondary) !important;
|
416 |
+
color: var(--text-primary) !important;
|
417 |
+
}
|
418 |
+
|
419 |
+
.tab-nav button.selected,
|
420 |
+
.gr-tab-nav button.selected {
|
421 |
+
background: var(--primary-gradient) !important;
|
422 |
+
color: var(--text-on-primary) !important;
|
423 |
+
border-color: #667eea !important;
|
424 |
+
}
|
425 |
+
|
426 |
+
/* Tab content */
|
427 |
+
.tabitem,
|
428 |
+
.gr-tabitem {
|
429 |
+
background: var(--card-bg) !important;
|
430 |
+
border: 1px solid var(--border-light) !important;
|
431 |
+
border-top: none !important;
|
432 |
+
border-radius: 0 0 12px 12px !important;
|
433 |
+
padding: 20px !important;
|
434 |
+
}
|
435 |
+
|
436 |
+
/* Output containers */
|
437 |
+
.output-container {
|
438 |
+
background: var(--card-bg);
|
439 |
+
border-radius: 12px;
|
440 |
+
border: 1px solid var(--border-light);
|
441 |
+
margin-top: 20px;
|
442 |
+
overflow: hidden;
|
443 |
+
}
|
444 |
+
|
445 |
+
/* Plot containers */
|
446 |
+
.plotly-graph-div {
|
447 |
+
background: var(--card-bg) !important;
|
448 |
+
border-radius: 8px !important;
|
449 |
}
|
450 |
|
451 |
/* Footer */
|
452 |
.footer {
|
453 |
text-align: center;
|
454 |
+
padding: 30px 20px;
|
455 |
+
color: var(--text-secondary);
|
456 |
font-size: 14px;
|
457 |
+
border-top: 1px solid var(--border-light);
|
458 |
+
margin-top: 40px;
|
459 |
+
background: var(--bg-secondary);
|
460 |
+
border-radius: 0 0 16px 16px;
|
461 |
+
line-height: 1.6;
|
462 |
+
}
|
463 |
+
|
464 |
+
.footer strong {
|
465 |
+
color: var(--text-primary);
|
466 |
+
font-weight: 600;
|
467 |
+
}
|
468 |
+
|
469 |
+
.footer em {
|
470 |
+
color: var(--text-accent);
|
471 |
+
font-style: normal;
|
472 |
+
}
|
473 |
+
|
474 |
+
/* Responsive design */
|
475 |
+
@media (max-width: 768px) {
|
476 |
+
.app-title {
|
477 |
+
padding: 30px 20px;
|
478 |
+
}
|
479 |
+
|
480 |
+
.app-title h1 {
|
481 |
+
font-size: 2rem;
|
482 |
+
}
|
483 |
+
|
484 |
+
.app-title p {
|
485 |
+
font-size: 1rem;
|
486 |
+
}
|
487 |
+
|
488 |
+
button.primary,
|
489 |
+
.gr-button-primary {
|
490 |
+
padding: 12px 24px !important;
|
491 |
+
font-size: 15px !important;
|
492 |
+
}
|
493 |
+
|
494 |
+
.examples-container {
|
495 |
+
padding: 16px;
|
496 |
+
}
|
497 |
+
|
498 |
+
.footer {
|
499 |
+
padding: 20px 15px;
|
500 |
+
font-size: 13px;
|
501 |
+
}
|
502 |
+
}
|
503 |
+
|
504 |
+
/* Smooth transitions for theme changes */
|
505 |
+
* {
|
506 |
+
transition: background-color 0.3s ease,
|
507 |
+
color 0.3s ease,
|
508 |
+
border-color 0.3s ease,
|
509 |
+
box-shadow 0.3s ease !important;
|
510 |
+
}
|
511 |
+
|
512 |
+
/* Custom scrollbar */
|
513 |
+
::-webkit-scrollbar {
|
514 |
+
width: 8px;
|
515 |
+
height: 8px;
|
516 |
+
}
|
517 |
+
|
518 |
+
::-webkit-scrollbar-track {
|
519 |
+
background: var(--bg-secondary);
|
520 |
+
border-radius: 4px;
|
521 |
+
}
|
522 |
+
|
523 |
+
::-webkit-scrollbar-thumb {
|
524 |
+
background: var(--border-strong);
|
525 |
+
border-radius: 4px;
|
526 |
+
}
|
527 |
+
|
528 |
+
::-webkit-scrollbar-thumb:hover {
|
529 |
+
background: var(--text-secondary);
|
530 |
+
}
|
531 |
+
|
532 |
+
/* Loading states */
|
533 |
+
.loading {
|
534 |
+
opacity: 0.7;
|
535 |
+
pointer-events: none;
|
536 |
+
}
|
537 |
+
|
538 |
+
/* Focus indicators for accessibility */
|
539 |
+
button:focus-visible,
|
540 |
+
textarea:focus-visible {
|
541 |
+
outline: 2px solid #667eea !important;
|
542 |
+
outline-offset: 2px !important;
|
543 |
+
}
|
544 |
+
|
545 |
+
/* High contrast mode support */
|
546 |
+
@media (prefers-contrast: high) {
|
547 |
+
:root {
|
548 |
+
--border-light: var(--border-strong);
|
549 |
+
--text-secondary: var(--text-primary);
|
550 |
+
}
|
551 |
+
}
|
552 |
+
|
553 |
+
/* Reduced motion support */
|
554 |
+
@media (prefers-reduced-motion: reduce) {
|
555 |
+
* {
|
556 |
+
transition: none !important;
|
557 |
+
animation: none !important;
|
558 |
+
}
|
559 |
+
}
|
560 |
+
"""
|
561 |
+
|
562 |
+
# Theme toggle script (JavaScript để detect và toggle theme)
|
563 |
+
theme_script = """
|
564 |
+
<script>
|
565 |
+
// Auto-detect system theme and apply appropriate class
|
566 |
+
function updateTheme() {
|
567 |
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
568 |
+
const root = document.documentElement;
|
569 |
+
|
570 |
+
if (isDark) {
|
571 |
+
root.classList.add('dark');
|
572 |
+
} else {
|
573 |
+
root.classList.remove('dark');
|
574 |
+
}
|
575 |
+
}
|
576 |
+
|
577 |
+
// Initial theme setup
|
578 |
+
updateTheme();
|
579 |
+
|
580 |
+
// Listen for system theme changes
|
581 |
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
|
582 |
+
|
583 |
+
// Manual theme toggle function (có thể được gọi từ button nếu cần)
|
584 |
+
window.toggleTheme = function() {
|
585 |
+
const root = document.documentElement;
|
586 |
+
root.classList.toggle('dark');
|
587 |
}
|
588 |
+
</script>
|
589 |
"""
|
590 |
|
591 |
# Tạo giao diện Gradio
|
592 |
with gr.Blocks(css=custom_css, title="Phân loại bình luận tiếng Việt - PhoBERT", theme=gr.themes.Soft()) as demo:
|
593 |
+
# Theme detection script
|
594 |
+
gr.HTML(theme_script)
|
595 |
+
|
596 |
# Header
|
597 |
gr.HTML("""
|
598 |
<div class="app-title">
|
599 |
<h1>🧠 Phân loại bình luận tiếng Việt</h1>
|
600 |
+
<p>Sử dụng mô hình PhoBERT để phân tích cảm xúc và độc hại trong bình luận mạng xã hội với giao diện thích ứng light/dark mode</p>
|
601 |
</div>
|
602 |
""")
|
603 |
|
604 |
with gr.Row():
|
605 |
with gr.Column(scale=1):
|
606 |
# Input section
|
607 |
+
gr.HTML("<h3>📝 Nhập bình luận cần phân tích</h3>")
|
608 |
|
609 |
input_text = gr.Textbox(
|
610 |
lines=4,
|
|
|
622 |
# Examples section
|
623 |
gr.HTML("""
|
624 |
<div class="examples-container">
|
625 |
+
<h3>💡 Ví dụ mẫu</h3>
|
626 |
+
<p>Nhấp vào các ví dụ bên dưới để thử nghiệm:</p>
|
627 |
</div>
|
628 |
""")
|
629 |
|
|
|
643 |
|
644 |
with gr.Column(scale=1):
|
645 |
# Output section
|
646 |
+
gr.HTML("<h3>📊 Kết quả phân tích</h3>")
|
647 |
|
648 |
with gr.Tabs():
|
649 |
with gr.TabItem("🎯 Kết quả chính", elem_id="main_result_tab"):
|
650 |
result_output = gr.HTML(
|
651 |
+
value="<div style='text-align: center; padding: 40px; color: var(--text-secondary);'>Nhập bình luận và nhấn 'Phân tích' để xem kết quả</div>"
|
652 |
)
|
653 |
|
654 |
with gr.TabItem("📈 Biểu đồ phân phối", elem_id="chart_tab"):
|
|
|
676 |
<p>
|
677 |
<strong>Mô hình:</strong> PhoBERT fine-tuned cho phân loại bình luận tiếng Việt<br>
|
678 |
<strong>Các nhãn:</strong> Tích cực • Tiêu cực • Trung tính • Độc hại<br>
|
679 |
+
<em>Được xây bởi Hà Văn Hải</em>
|
680 |
</p>
|
681 |
</div>
|
682 |
""")
|