Update app.py
Browse files
app.py
CHANGED
|
@@ -1,118 +1,90 @@
|
|
| 1 |
-
from flask import Flask
|
| 2 |
|
| 3 |
app = Flask(__name__)
|
| 4 |
|
| 5 |
-
#
|
| 6 |
html_content = """
|
| 7 |
<!DOCTYPE html>
|
| 8 |
<html lang="vi">
|
| 9 |
<head>
|
| 10 |
<meta charset="UTF-8">
|
| 11 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 12 |
-
<title>
|
| 13 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
<style>
|
| 15 |
-
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
|
| 16 |
body {
|
| 17 |
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 18 |
-
background-color: #
|
| 19 |
color: #f1f5f9;
|
| 20 |
margin: 0;
|
| 21 |
display: flex;
|
| 22 |
align-items: center;
|
| 23 |
justify-content: center;
|
| 24 |
min-height: 100vh;
|
| 25 |
-
|
| 26 |
-
padding: 20px 0;
|
| 27 |
}
|
| 28 |
.mini-card {
|
| 29 |
background: #1e293b;
|
| 30 |
-
border: 1px solid rgba(255, 255, 255, 0.
|
| 31 |
-
width:
|
| 32 |
-
max-width:
|
| 33 |
-
max-height:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
overflow-y: auto;
|
| 35 |
-
|
| 36 |
-
position: relative;
|
| 37 |
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
|
| 38 |
-
scrollbar-width: thin;
|
| 39 |
}
|
| 40 |
-
.mini-card::-webkit-scrollbar {
|
| 41 |
-
.
|
| 42 |
-
|
| 43 |
-
.
|
| 44 |
-
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
| 45 |
-
|
| 46 |
-
.correct-anim { background: #065f46 !important; border-color: #10b981 !important; transform: scale(1.02); }
|
| 47 |
-
.wrong-anim { animation: shake 0.4s ease; background: #7f1d1d !important; border-color: #ef4444 !important; }
|
| 48 |
-
@keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-6px); } 75% { transform: translateX(6px); } }
|
| 49 |
-
|
| 50 |
.progress-dot { width: 8px; height: 8px; border-radius: 50%; background: #475569; transition: all 0.4s ease; }
|
| 51 |
.progress-dot.active { background: #3b82f6; width: 24px; border-radius: 12px; }
|
|
|
|
|
|
|
| 52 |
</style>
|
| 53 |
</head>
|
| 54 |
<body>
|
| 55 |
-
<div id="app" class="mini-card
|
| 56 |
-
|
| 57 |
-
<div class="flex justify-between items-center mb-8 border-b border-slate-700/50 pb-4">
|
| 58 |
<div>
|
| 59 |
-
<
|
| 60 |
-
<
|
| 61 |
</div>
|
| 62 |
-
<div id="dots-container" class="flex gap-
|
| 63 |
<div class="progress-dot active"></div><div class="progress-dot"></div><div class="progress-dot"></div>
|
| 64 |
</div>
|
| 65 |
</div>
|
| 66 |
-
|
| 67 |
<div id="quiz-container">
|
| 68 |
-
<h2 id="question-text" class="text-xl font-bold mb-6
|
| 69 |
-
<div id="options-grid" class="space-y-3"></div>
|
| 70 |
</div>
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
<span class="text-blue-400 text-lg">📝</span>
|
| 76 |
-
<span class="text-blue-400 font-bold text-xs uppercase tracking-tighter">Giải thích chi tiết:</span>
|
| 77 |
</div>
|
| 78 |
-
<p id="explanation-text" class="text-[
|
| 79 |
-
<button id="next-btn" class="w-full mt-
|
| 80 |
-
Tiếp tục thử thách
|
| 81 |
-
</button>
|
| 82 |
</div>
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
<
|
| 86 |
-
<
|
| 87 |
-
<
|
| 88 |
-
<button onclick="location.reload()" class="w-full py-4 bg-slate-800 hover:bg-slate-700 rounded-2xl text-sm font-bold border border-slate-700">Thực hiện lại</button>
|
| 89 |
</div>
|
| 90 |
</div>
|
| 91 |
-
|
| 92 |
<script>
|
| 93 |
const quizData = [
|
| 94 |
-
{
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
correct: 0,
|
| 98 |
-
info: "TP.HCM là 'vùng đất hứa' tụ hội cư dân từ khắp mọi miền đất nước. Mỗi nhóm người mang theo phương ngữ đặc trưng (Bắc, Trung, Tây Nam Bộ...), khi hòa quyện với nhau đã tạo nên một hệ thống từ vựng cực kỳ phong phú, biến thành phố thành một 'bảo tàng ngôn ngữ sống' đầy thú vị."
|
| 99 |
-
},
|
| 100 |
-
{
|
| 101 |
-
q: "Đặc điểm nổi bật nhất trong lối nói của người Sài Gòn là gì?",
|
| 102 |
-
options: ["Sử dụng nhiều từ hán việt", "Bộc trực, hào sảng và giản dị", "Ưa chuộng sự cầu kỳ, lễ nghi", "Nói giảm nói tránh nhiều"],
|
| 103 |
-
correct: 1,
|
| 104 |
-
info: "Lối sống mở và phóng khoáng đã hình thành nên phong cách giao tiếp 'nói thẳng, nói thật'. Người Sài Gòn thường lược bỏ sự khách sáo rườm rà, thay vào đó là cách xưng hô thân thiện (anh Hai, chị Ba, chú Tư...) và những từ ngữ bộc lộ sự chân thành ngay lập tức."
|
| 105 |
-
},
|
| 106 |
-
{
|
| 107 |
-
q: "Làm thế nào để bảo tồn bản sắc ngôn ngữ thành phố hiệu quả nhất?",
|
| 108 |
-
options: ["Hạn chế sử dụng tiếng lóng", "Chuẩn hóa giọng nói duy nhất", "Kết hợp giáo dục và công nghệ số", "Chỉ sử dụng trong văn bản chính thức"],
|
| 109 |
-
correct: 2,
|
| 110 |
-
info: "Trong kỷ nguyên số, việc bảo tồn không chỉ là giữ gìn trong sách vở. Chúng ta cần 'số hóa' các phương ngữ, xây dựng từ điển điện tử và lồng ghép vào giáo dục thực tiễn. Điều này giúp thế hệ trẻ vừa tiếp cận được công nghệ, vừa không làm mất đi 'cái gốc' tiếng nói của cha ông."
|
| 111 |
-
}
|
| 112 |
];
|
| 113 |
-
|
| 114 |
let current = 0, score = 0, active = true;
|
| 115 |
-
|
| 116 |
function init() {
|
| 117 |
active = true;
|
| 118 |
const data = quizData[current];
|
|
@@ -120,51 +92,34 @@ html_content = """
|
|
| 120 |
const grid = document.getElementById('options-grid');
|
| 121 |
grid.innerHTML = '';
|
| 122 |
document.getElementById('explanation-box').classList.add('hidden');
|
| 123 |
-
document.getElementById('step-label').textContent = `
|
| 124 |
-
|
| 125 |
-
document.querySelectorAll('.progress-dot').forEach((dot, i) => {
|
| 126 |
-
dot.className = i === current ? 'progress-dot active' : 'progress-dot';
|
| 127 |
-
});
|
| 128 |
-
|
| 129 |
data.options.forEach((opt, i) => {
|
| 130 |
const btn = document.createElement('button');
|
| 131 |
-
btn.className = 'w-full p-4 text-left text-sm rounded-2xl
|
| 132 |
-
btn.innerHTML = `<span class="flex-none w-6 h-6 rounded-lg bg-slate-700 flex items-center justify-center text-[10px] font-bold text-blue-400">${String.fromCharCode(65+i)}</span><span class="text-slate-300 leading-tight">${opt}</span>`;
|
| 133 |
-
|
| 134 |
btn.onclick = () => {
|
| 135 |
if(!active) return; active = false;
|
| 136 |
const buttons = grid.querySelectorAll('button');
|
| 137 |
-
if(i === data.correct) {
|
| 138 |
-
|
| 139 |
-
score++;
|
| 140 |
-
} else {
|
| 141 |
-
btn.classList.add('wrong-anim');
|
| 142 |
-
buttons[data.correct].classList.add('correct-anim');
|
| 143 |
-
}
|
| 144 |
document.getElementById('explanation-text').textContent = data.info;
|
| 145 |
document.getElementById('explanation-box').classList.remove('hidden');
|
| 146 |
-
|
| 147 |
-
setTimeout(() => { document.querySelector('.mini-card').scrollTo({top: 500, behavior: 'smooth'}); }, 100);
|
| 148 |
};
|
| 149 |
grid.appendChild(btn);
|
| 150 |
});
|
| 151 |
}
|
| 152 |
-
|
| 153 |
document.getElementById('next-btn').onclick = () => {
|
| 154 |
current++;
|
| 155 |
-
if(current < 3) {
|
| 156 |
-
|
| 157 |
-
init();
|
| 158 |
-
} else {
|
| 159 |
document.getElementById('quiz-container').classList.add('hidden');
|
| 160 |
document.getElementById('explanation-box').classList.add('hidden');
|
| 161 |
document.getElementById('end-screen').classList.remove('hidden');
|
| 162 |
document.getElementById('final-score').textContent = `${score}/3`;
|
| 163 |
-
document.getElementById('step-label').parentElement.classList.add('hidden');
|
| 164 |
-
document.getElementById('dots-container').classList.add('hidden');
|
| 165 |
}
|
| 166 |
};
|
| 167 |
-
|
| 168 |
init();
|
| 169 |
</script>
|
| 170 |
</body>
|
|
@@ -173,7 +128,12 @@ html_content = """
|
|
| 173 |
|
| 174 |
@app.route("/")
|
| 175 |
def index():
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
if __name__ == "__main__":
|
| 179 |
app.run(host="0.0.0.0", port=7860)
|
|
|
|
| 1 |
+
from flask import Flask, make_response
|
| 2 |
|
| 3 |
app = Flask(__name__)
|
| 4 |
|
| 5 |
+
# Giao diện Quiz đã được tối ưu
|
| 6 |
html_content = """
|
| 7 |
<!DOCTYPE html>
|
| 8 |
<html lang="vi">
|
| 9 |
<head>
|
| 10 |
<meta charset="UTF-8">
|
| 11 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 12 |
+
<title>Sắc màu ngôn ngữ TP.HCM</title>
|
| 13 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 14 |
<style>
|
| 15 |
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
|
| 16 |
body {
|
| 17 |
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 18 |
+
background-color: #0f172a;
|
| 19 |
color: #f1f5f9;
|
| 20 |
margin: 0;
|
| 21 |
display: flex;
|
| 22 |
align-items: center;
|
| 23 |
justify-content: center;
|
| 24 |
min-height: 100vh;
|
| 25 |
+
padding: 1rem;
|
|
|
|
| 26 |
}
|
| 27 |
.mini-card {
|
| 28 |
background: #1e293b;
|
| 29 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 30 |
+
width: 100%;
|
| 31 |
+
max-width: 450px;
|
| 32 |
+
max-height: 95vh;
|
| 33 |
+
border-radius: 32px;
|
| 34 |
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
| 35 |
+
padding: 2rem;
|
| 36 |
+
display: flex;
|
| 37 |
+
flex-direction: column;
|
| 38 |
overflow-y: auto;
|
| 39 |
+
scrollbar-width: none;
|
|
|
|
|
|
|
|
|
|
| 40 |
}
|
| 41 |
+
.mini-card::-webkit-scrollbar { display: none; }
|
| 42 |
+
.option-btn { background: rgba(51, 65, 85, 0.4); border: 1px solid rgba(71, 85, 105, 0.5); transition: all 0.3s ease; }
|
| 43 |
+
.correct-anim { background: #064e3b !important; border-color: #10b981 !important; color: #ecfdf5 !important; }
|
| 44 |
+
.wrong-anim { background: #7f1d1d !important; border-color: #ef4444 !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
.progress-dot { width: 8px; height: 8px; border-radius: 50%; background: #475569; transition: all 0.4s ease; }
|
| 46 |
.progress-dot.active { background: #3b82f6; width: 24px; border-radius: 12px; }
|
| 47 |
+
#explanation-box { animation: slideIn 0.4s ease-out forwards; }
|
| 48 |
+
@keyframes slideIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
| 49 |
</style>
|
| 50 |
</head>
|
| 51 |
<body>
|
| 52 |
+
<div id="app" class="mini-card">
|
| 53 |
+
<div class="flex justify-between items-center mb-6 pb-4 border-b border-slate-700/50">
|
|
|
|
| 54 |
<div>
|
| 55 |
+
<p class="text-[10px] font-bold uppercase tracking-widest text-blue-500 mb-1">Hành trình ngôn ngữ</p>
|
| 56 |
+
<h4 id="step-label" class="text-xs font-semibold text-slate-400 uppercase">Câu 1 / 3</h4>
|
| 57 |
</div>
|
| 58 |
+
<div id="dots-container" class="flex gap-1.5">
|
| 59 |
<div class="progress-dot active"></div><div class="progress-dot"></div><div class="progress-dot"></div>
|
| 60 |
</div>
|
| 61 |
</div>
|
|
|
|
| 62 |
<div id="quiz-container">
|
| 63 |
+
<h2 id="question-text" class="text-xl font-bold leading-tight mb-6 text-white">Đang tải...</h2>
|
| 64 |
+
<div id="options-grid" class="space-y-3 mb-6"></div>
|
| 65 |
</div>
|
| 66 |
+
<div id="explanation-box" class="hidden p-5 bg-blue-600/10 rounded-2xl border border-blue-500/20">
|
| 67 |
+
<div class="flex items-center gap-2 mb-3">
|
| 68 |
+
<span class="text-lg">💡</span>
|
| 69 |
+
<span class="text-[10px] font-black uppercase tracking-widest text-blue-400">Phân tích chuyên sâu</span>
|
|
|
|
|
|
|
| 70 |
</div>
|
| 71 |
+
<p id="explanation-text" class="text-[13.5px] leading-relaxed text-slate-300 text-justify italic font-medium"></p>
|
| 72 |
+
<button id="next-btn" class="w-full mt-6 py-4 bg-blue-600 hover:bg-blue-500 text-white text-sm font-bold rounded-xl shadow-lg transition-all active:scale-95">Tiếp tục</button>
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
+
<div id="end-screen" class="hidden text-center py-8">
|
| 75 |
+
<div class="text-6xl mb-6">🏆</div>
|
| 76 |
+
<h3 class="text-2xl font-bold text-white mb-2">Hoàn thành!</h3>
|
| 77 |
+
<div id="final-score" class="text-7xl font-black text-blue-400 mb-10">0/3</div>
|
| 78 |
+
<button onclick="location.reload()" class="w-full py-4 bg-slate-800 border border-slate-700 rounded-2xl font-bold text-sm">Làm lại</button>
|
|
|
|
| 79 |
</div>
|
| 80 |
</div>
|
|
|
|
| 81 |
<script>
|
| 82 |
const quizData = [
|
| 83 |
+
{ q: "Tại sao ngôn ngữ TP.HCM lại có sự pha trộn đặc trưng đến vậy?", options: ["Do vị trí địa lý", "Do chính sách", "Do giao thoa cư dân", "Do phương Tây"], correct: 2, info: "Sài Gòn là vùng đất 'mở', đón nhận di cư từ khắp miền Bắc, Trung, Tây. Sự cộng hưởng này t��o ra hệ thống từ vựng phong phú và bản sắc riêng không trộn lẫn." },
|
| 84 |
+
{ q: "Tính cách 'hào sảng' ảnh hưởng thế nào đến cách nói chuyện?", options: ["Cầu kỳ, lễ nghĩa", "Bộc trực, giản dị", "Xa cách, giữ kẽ", "Dùng từ bác học"], correct: 1, info: "Lối nói 'nghĩ sao nói vậy' phản chiếu sự hào sảng. Họ chuộng từ thân mật (anh Hai, chị Ba, cưng...) giúp xóa tan khoảng cách ngay lần đầu gặp mặt." },
|
| 85 |
+
{ q: "Hướng đi bền vững nhất để giữ gìn ngôn ngữ Sài Gòn?", options: ["Dùng trong gia đình", "Cấm tiếng lóng", "Giáo dục & Công nghệ", "Cố định phát âm"], correct: 2, info: "Bảo tồn không phải là giữ khư khư, mà là làm cho ngôn ngữ đó tiếp tục sống trên podcast, phim ảnh để thế hệ trẻ luôn yêu mến nó." }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
];
|
|
|
|
| 87 |
let current = 0, score = 0, active = true;
|
|
|
|
| 88 |
function init() {
|
| 89 |
active = true;
|
| 90 |
const data = quizData[current];
|
|
|
|
| 92 |
const grid = document.getElementById('options-grid');
|
| 93 |
grid.innerHTML = '';
|
| 94 |
document.getElementById('explanation-box').classList.add('hidden');
|
| 95 |
+
document.getElementById('step-label').textContent = `Câu ${current + 1} / 3`;
|
| 96 |
+
document.querySelectorAll('.progress-dot').forEach((dot, i) => dot.className = i === current ? 'progress-dot active' : 'progress-dot');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
data.options.forEach((opt, i) => {
|
| 98 |
const btn = document.createElement('button');
|
| 99 |
+
btn.className = 'option-btn w-full p-4 text-left text-sm rounded-2xl flex gap-3';
|
| 100 |
+
btn.innerHTML = `<span class="flex-none w-6 h-6 rounded-lg bg-slate-700/50 flex items-center justify-center text-[10px] font-bold text-blue-400">${String.fromCharCode(65+i)}</span><span class="text-slate-300 leading-tight">${opt}</span>`;
|
|
|
|
| 101 |
btn.onclick = () => {
|
| 102 |
if(!active) return; active = false;
|
| 103 |
const buttons = grid.querySelectorAll('button');
|
| 104 |
+
if(i === data.correct) { btn.classList.add('correct-anim'); score++; }
|
| 105 |
+
else { btn.classList.add('wrong-anim'); buttons[data.correct].classList.add('correct-anim'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
document.getElementById('explanation-text').textContent = data.info;
|
| 107 |
document.getElementById('explanation-box').classList.remove('hidden');
|
| 108 |
+
setTimeout(() => { document.querySelector('.mini-card').scrollTo({top: 1000, behavior: 'smooth'}); }, 100);
|
|
|
|
| 109 |
};
|
| 110 |
grid.appendChild(btn);
|
| 111 |
});
|
| 112 |
}
|
|
|
|
| 113 |
document.getElementById('next-btn').onclick = () => {
|
| 114 |
current++;
|
| 115 |
+
if(current < 3) { document.querySelector('.mini-card').scrollTo({top: 0, behavior: 'smooth'}); init(); }
|
| 116 |
+
else {
|
|
|
|
|
|
|
| 117 |
document.getElementById('quiz-container').classList.add('hidden');
|
| 118 |
document.getElementById('explanation-box').classList.add('hidden');
|
| 119 |
document.getElementById('end-screen').classList.remove('hidden');
|
| 120 |
document.getElementById('final-score').textContent = `${score}/3`;
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
};
|
|
|
|
| 123 |
init();
|
| 124 |
</script>
|
| 125 |
</body>
|
|
|
|
| 128 |
|
| 129 |
@app.route("/")
|
| 130 |
def index():
|
| 131 |
+
# Tạo response và thêm Header để cho phép Canva (hoặc bất kỳ site nào) nhúng vào
|
| 132 |
+
response = make_response(html_content)
|
| 133 |
+
# Loại bỏ các hạn chế về Iframe
|
| 134 |
+
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
| 135 |
+
response.headers['Content-Security-Policy'] = "frame-ancestors *;"
|
| 136 |
+
return response
|
| 137 |
|
| 138 |
if __name__ == "__main__":
|
| 139 |
app.run(host="0.0.0.0", port=7860)
|