Create exercise_ui.js
Browse files- js/ui/exercise_ui.js +276 -0
js/ui/exercise_ui.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ExerciseUI {
|
| 2 |
+
constructor() {
|
| 3 |
+
this.currentExercise = null;
|
| 4 |
+
this.answerSubmitted = false;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
async render(exerciseData) {
|
| 8 |
+
this.currentExercise = exerciseData;
|
| 9 |
+
this.answerSubmitted = false;
|
| 10 |
+
|
| 11 |
+
const exerciseContent = document.getElementById('exerciseContent');
|
| 12 |
+
const exerciseFeedback = document.getElementById('exerciseFeedback');
|
| 13 |
+
|
| 14 |
+
if (!exerciseContent) return;
|
| 15 |
+
|
| 16 |
+
// پاک کردن فیدبک قبلی
|
| 17 |
+
if (exerciseFeedback) {
|
| 18 |
+
exerciseFeedback.innerHTML = '';
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
let html = `
|
| 22 |
+
<div class="exercise-header">
|
| 23 |
+
<h4>${this.escapeHtml(exerciseData.exercise.question)}</h4>
|
| 24 |
+
${exerciseData.exercise.description ?
|
| 25 |
+
`<p class="exercise-description">${this.escapeHtml(exerciseData.exercise.description)}</p>` : ''}
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="exercise-body">
|
| 29 |
+
<div class="answer-section">
|
| 30 |
+
<label for="exerciseAnswer" class="answer-label">پاسخ خود را بنویسید:</label>
|
| 31 |
+
<textarea
|
| 32 |
+
id="exerciseAnswer"
|
| 33 |
+
rows="6"
|
| 34 |
+
placeholder="پاسخ خود را اینجا وارد کنید..."
|
| 35 |
+
${this.answerSubmitted ? 'disabled' : ''}
|
| 36 |
+
></textarea>
|
| 37 |
+
|
| 38 |
+
${exerciseData.exercise.hint ?
|
| 39 |
+
`<div class="exercise-hint">
|
| 40 |
+
<strong>💡 نکته:</strong> ${this.escapeHtml(exerciseData.exercise.hint)}
|
| 41 |
+
</div>` : ''}
|
| 42 |
+
|
| 43 |
+
<div class="exercise-actions">
|
| 44 |
+
<button
|
| 45 |
+
id="submitExercise"
|
| 46 |
+
class="btn btn-primary"
|
| 47 |
+
onclick="exerciseUI.submitAnswer()"
|
| 48 |
+
${this.answerSubmitted ? 'disabled' : ''}
|
| 49 |
+
>
|
| 50 |
+
ارسال پاسخ
|
| 51 |
+
</button>
|
| 52 |
+
<button
|
| 53 |
+
class="btn btn-secondary"
|
| 54 |
+
onclick="exerciseUI.showSolution()"
|
| 55 |
+
>
|
| 56 |
+
مشاهده راهحل
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="exercise-info">
|
| 62 |
+
<div class="info-card">
|
| 63 |
+
<h5>📋 انتظارات ما</h5>
|
| 64 |
+
<ul class="expected-keywords">
|
| 65 |
+
${exerciseData.exercise.expected_keywords.map(keyword =>
|
| 66 |
+
`<li><code>${this.escapeHtml(keyword)}</code></li>`
|
| 67 |
+
).join('')}
|
| 68 |
+
</ul>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
<div class="info-card">
|
| 72 |
+
<h5>🎯 معیارهای ارزیابی</h5>
|
| 73 |
+
<p>پاسخ شما بر اساس تطابق با کلمات کلیدی بالا ارزیابی میشود.</p>
|
| 74 |
+
<p>حداقل ${Math.round((exerciseData.exercise.match_threshold || 0.6) * 100)}% تطابق مورد نیاز است.</p>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
`;
|
| 79 |
+
|
| 80 |
+
exerciseContent.innerHTML = html;
|
| 81 |
+
|
| 82 |
+
// تنظیم event listener برای textarea
|
| 83 |
+
const textarea = document.getElementById('exerciseAnswer');
|
| 84 |
+
if (textarea && !this.answerSubmitted) {
|
| 85 |
+
textarea.addEventListener('input', this.debounce(this.autoSaveAnswer.bind(this), 1000));
|
| 86 |
+
this.loadSavedAnswer();
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
async submitAnswer() {
|
| 91 |
+
const answerTextarea = document.getElementById('exerciseAnswer');
|
| 92 |
+
const submitButton = document.getElementById('submitExercise');
|
| 93 |
+
|
| 94 |
+
if (!answerTextarea || !submitButton) return;
|
| 95 |
+
|
| 96 |
+
const answer = answerTextarea.value.trim();
|
| 97 |
+
|
| 98 |
+
if (!answer) {
|
| 99 |
+
Utils.showNotification('لطفاً پاسخ خود را وارد کنید', 'error');
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// غیرفعال کردن دکمه و نمایش حالت loading
|
| 104 |
+
submitButton.disabled = true;
|
| 105 |
+
submitButton.innerHTML = '⏳ در حال بررسی...';
|
| 106 |
+
|
| 107 |
+
try {
|
| 108 |
+
const result = await learningLogic.checkExerciseAnswer(
|
| 109 |
+
this.currentExercise.day,
|
| 110 |
+
answer
|
| 111 |
+
);
|
| 112 |
+
|
| 113 |
+
this.answerSubmitted = true;
|
| 114 |
+
this.showResult(result);
|
| 115 |
+
this.clearSavedAnswer();
|
| 116 |
+
|
| 117 |
+
// ارسال event برای بهروزرسانی سایر بخشها
|
| 118 |
+
document.dispatchEvent(new CustomEvent('exerciseSubmitted', {
|
| 119 |
+
detail: {
|
| 120 |
+
exercise: this.currentExercise,
|
| 121 |
+
result: result
|
| 122 |
+
}
|
| 123 |
+
}));
|
| 124 |
+
|
| 125 |
+
} catch (error) {
|
| 126 |
+
console.error('Error submitting exercise:', error);
|
| 127 |
+
Utils.showNotification('خطا در ارسال پاسخ', 'error');
|
| 128 |
+
submitButton.disabled = false;
|
| 129 |
+
submitButton.innerHTML = 'ارسال پاسخ';
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
showResult(result) {
|
| 134 |
+
const exerciseFeedback = document.getElementById('exerciseFeedback');
|
| 135 |
+
if (!exerciseFeedback) return;
|
| 136 |
+
|
| 137 |
+
const answerTextarea = document.getElementById('exerciseAnswer');
|
| 138 |
+
const submitButton = document.getElementById('submitExercise');
|
| 139 |
+
|
| 140 |
+
if (answerTextarea) {
|
| 141 |
+
answerTextarea.disabled = true;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
if (submitButton) {
|
| 145 |
+
submitButton.disabled = true;
|
| 146 |
+
submitButton.innerHTML = '✅ پاسخ ارسال شده';
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
let html = `
|
| 150 |
+
<div class="exercise-result ${result.isCorrect ? 'success' : 'warning'}">
|
| 151 |
+
<div class="result-header">
|
| 152 |
+
<h4>${result.isCorrect ? '✅ پاسخ صحیح' : '❌ نیاز به بهبود'}</h4>
|
| 153 |
+
<div class="reward-badge">${result.reward} امتیاز</div>
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
<div class="result-body">
|
| 157 |
+
<p><strong>پیام:</strong> ${result.feedback.message}</p>
|
| 158 |
+
|
| 159 |
+
<div class="keywords-analysis">
|
| 160 |
+
<h5>📊 تحلیل کلمات کلیدی:</h5>
|
| 161 |
+
<div class="keywords-grid">
|
| 162 |
+
<div class="matched-keywords">
|
| 163 |
+
<h6>✅ کلمات یافت شده:</h6>
|
| 164 |
+
${result.matchedKeywords.length > 0 ?
|
| 165 |
+
`<ul>${result.matchedKeywords.map(kw => `<li><code>${this.escapeHtml(kw)}</code></li>`).join('')}</ul>` :
|
| 166 |
+
'<p>هیچ کلمهای یافت نشد</p>'
|
| 167 |
+
}
|
| 168 |
+
</div>
|
| 169 |
+
<div class="missing-keywords">
|
| 170 |
+
<h6>❌ کلمات مفقوده:</h6>
|
| 171 |
+
${result.expectedKeywords.filter(kw => !result.matchedKeywords.includes(kw)).length > 0 ?
|
| 172 |
+
`<ul>${result.expectedKeywords.filter(kw => !result.matchedKeywords.includes(kw))
|
| 173 |
+
.map(kw => `<li><code>${this.escapeHtml(kw)}</code></li>`).join('')}</ul>` :
|
| 174 |
+
'<p>همه کلمات یافت شدند</p>'
|
| 175 |
+
}
|
| 176 |
+
</div>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
${!result.isCorrect && result.hint ?
|
| 181 |
+
`<div class="improvement-hint">
|
| 182 |
+
<strong>💡 راهنمایی بهبود:</strong> ${this.escapeHtml(result.hint)}
|
| 183 |
+
</div>` : ''
|
| 184 |
+
}
|
| 185 |
+
</div>
|
| 186 |
+
|
| 187 |
+
<div class="result-actions">
|
| 188 |
+
<button class="btn btn-outline" onclick="exerciseUI.tryAgain()">
|
| 189 |
+
🔄 تلاش مجدد
|
| 190 |
+
</button>
|
| 191 |
+
<button class="btn btn-primary" onclick="exerciseUI.nextExercise()">
|
| 192 |
+
➡️ تمرین بعدی
|
| 193 |
+
</button>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
`;
|
| 197 |
+
|
| 198 |
+
exerciseFeedback.innerHTML = html;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
tryAgain() {
|
| 202 |
+
this.answerSubmitted = false;
|
| 203 |
+
this.render(this.currentExercise);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
nextExercise() {
|
| 207 |
+
// در این نسخه ساده، به روز بعد میرویم
|
| 208 |
+
// در نسخههای آینده میتوان منطق پیچیدهتری پیادهسازی کرد
|
| 209 |
+
const nextDay = this.currentExercise.day + 1;
|
| 210 |
+
document.dispatchEvent(new CustomEvent('dayChanged', {
|
| 211 |
+
detail: { day: nextDay }
|
| 212 |
+
}));
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
showSolution() {
|
| 216 |
+
if (!this.currentExercise) return;
|
| 217 |
+
|
| 218 |
+
// در این نسخه ساده، فقط کلمات کلیدی نمایش داده میشوند
|
| 219 |
+
// در نسخههای آینده میتوان راهحل کامل را نمایش داد
|
| 220 |
+
const keywords = this.currentExercise.exercise.expected_keywords;
|
| 221 |
+
Utils.showNotification(
|
| 222 |
+
`کلمات کلیدی مورد انتظار: ${keywords.join('، ')}`,
|
| 223 |
+
'info'
|
| 224 |
+
);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
autoSaveAnswer() {
|
| 228 |
+
const answerTextarea = document.getElementById('exerciseAnswer');
|
| 229 |
+
if (!answerTextarea || this.answerSubmitted) return;
|
| 230 |
+
|
| 231 |
+
const answer = answerTextarea.value;
|
| 232 |
+
const key = `exercise_${this.currentExercise.day}_answer`;
|
| 233 |
+
Utils.saveToLocalStorage(key, answer);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
loadSavedAnswer() {
|
| 237 |
+
const answerTextarea = document.getElementById('exerciseAnswer');
|
| 238 |
+
if (!answerTextarea) return;
|
| 239 |
+
|
| 240 |
+
const key = `exercise_${this.currentExercise.day}_answer`;
|
| 241 |
+
const savedAnswer = Utils.loadFromLocalStorage(key);
|
| 242 |
+
|
| 243 |
+
if (savedAnswer) {
|
| 244 |
+
answerTextarea.value = savedAnswer;
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
clearSavedAnswer() {
|
| 249 |
+
const key = `exercise_${this.currentExercise.day}_answer`;
|
| 250 |
+
localStorage.removeItem(key);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
escapeHtml(unsafe) {
|
| 254 |
+
return unsafe
|
| 255 |
+
.replace(/&/g, "&")
|
| 256 |
+
.replace(/</g, "<")
|
| 257 |
+
.replace(/>/g, ">")
|
| 258 |
+
.replace(/"/g, """)
|
| 259 |
+
.replace(/'/g, "'");
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
debounce(func, wait) {
|
| 263 |
+
let timeout;
|
| 264 |
+
return function executedFunction(...args) {
|
| 265 |
+
const later = () => {
|
| 266 |
+
clearTimeout(timeout);
|
| 267 |
+
func(...args);
|
| 268 |
+
};
|
| 269 |
+
clearTimeout(timeout);
|
| 270 |
+
timeout = setTimeout(later, wait);
|
| 271 |
+
};
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
// ایجاد نمونه اصلی
|
| 276 |
+
const exerciseUI = new ExerciseUI();
|