Spaces:
Running
Running
Upload 9 files
Browse files- src/views/StudentView.js +53 -30
src/views/StudentView.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js";
|
| 2 |
-
import { generateMonsterSVG, getNextMonster, MONSTER_STAGES } from "../utils/monsterUtils.js";
|
| 3 |
|
| 4 |
|
| 5 |
// Cache challenges locally
|
|
@@ -198,9 +198,12 @@ export async function renderStudentView() {
|
|
| 198 |
}
|
| 199 |
|
| 200 |
return `
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
| 204 |
style="transform: scale(${currentScale}); animation: walk-float 6s ease-in-out infinite;">
|
| 205 |
|
| 206 |
<div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
|
|
@@ -213,8 +216,9 @@ export async function renderStudentView() {
|
|
| 213 |
</div>
|
| 214 |
</div>
|
| 215 |
|
| 216 |
-
<!--
|
| 217 |
-
|
|
|
|
| 218 |
<div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
|
| 219 |
<div class="flex flex-col items-center">
|
| 220 |
<div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
|
|
@@ -226,9 +230,10 @@ export async function renderStudentView() {
|
|
| 226 |
</button>
|
| 227 |
</div>
|
| 228 |
</div>
|
| 229 |
-
` : ''
|
|
|
|
| 230 |
|
| 231 |
-
<!--
|
| 232 |
<div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 mt-6 text-left pointer-events-auto shadow-2xl min-w-[120px]">
|
| 233 |
<div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
|
| 234 |
<div class="space-y-1 mt-1">
|
|
@@ -237,20 +242,20 @@ export async function renderStudentView() {
|
|
| 237 |
<div class="flex justify-between"><span>⚔️ 任務:</span> <span class="text-yellow-400 font-bold">${totalCompleted}</span></div>
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
-
</div>
|
| 241 |
|
| 242 |
<style>
|
| 243 |
@keyframes breathe {
|
| 244 |
-
0%, 100% { transform: translateY(0); filter: brightness(1); }
|
| 245 |
-
50% {
|
| 246 |
}
|
| 247 |
@keyframes walk-float {
|
| 248 |
-
0%, 100% { transform: translateX(0) scale(${currentScale}); }
|
| 249 |
-
25% {
|
| 250 |
-
|
| 251 |
}
|
| 252 |
</style>
|
| 253 |
-
|
| 254 |
};
|
| 255 |
|
| 256 |
// Inject Initial Monster UI
|
|
@@ -287,7 +292,7 @@ export async function renderStudentView() {
|
|
| 287 |
|
| 288 |
// Accordion Layout
|
| 289 |
return `
|
| 290 |
-
|
| 291 |
<header class="flex justify-end items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
|
| 292 |
<div class="flex flex-col items-end">
|
| 293 |
<div class="flex items-center space-x-2">
|
|
@@ -305,12 +310,30 @@ export async function renderStudentView() {
|
|
| 305 |
<div class="space-y-4">
|
| 306 |
${['beginner', 'intermediate', 'advanced'].map(level => {
|
| 307 |
const tasks = levelGroups[level] || [];
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
// Count completed
|
| 310 |
const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
|
|
|
|
| 311 |
|
| 312 |
return `
|
| 313 |
-
<details class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${
|
| 314 |
<summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
|
| 315 |
<div class="flex items-center space-x-3">
|
| 316 |
<h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
|
|
@@ -333,15 +356,15 @@ export async function renderStudentView() {
|
|
| 333 |
}).join('')}
|
| 334 |
</div>
|
| 335 |
|
| 336 |
-
<!--
|
| 337 |
<button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
|
| 338 |
title="查看同學作業">
|
| 339 |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 340 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
|
| 341 |
</svg>
|
| 342 |
</button>
|
| 343 |
-
</div>
|
| 344 |
-
|
| 345 |
}
|
| 346 |
|
| 347 |
export function setupStudentEvents() {
|
|
@@ -371,8 +394,8 @@ export function setupStudentEvents() {
|
|
| 371 |
};
|
| 372 |
|
| 373 |
window.submitLevel = async (challengeId) => {
|
| 374 |
-
const input = document.getElementById(`input-${challengeId}`);
|
| 375 |
-
const errorMsg = document.getElementById(`error-${challengeId}`);
|
| 376 |
const prompt = input.value;
|
| 377 |
const roomCode = localStorage.getItem('vibecoding_room_code');
|
| 378 |
const userId = localStorage.getItem('vibecoding_user_id');
|
|
@@ -418,7 +441,7 @@ export function setupStudentEvents() {
|
|
| 418 |
const newCardHTML = renderTaskCard(challenge, newProgress);
|
| 419 |
|
| 420 |
// 3. Replace in DOM
|
| 421 |
-
const oldCard = document.getElementById(`card-${challengeId}`);
|
| 422 |
if (oldCard) {
|
| 423 |
oldCard.outerHTML = newCardHTML;
|
| 424 |
} else {
|
|
@@ -485,12 +508,12 @@ function renderPeerModal() {
|
|
| 485 |
let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
|
| 486 |
if (cachedChallenges.length > 0) {
|
| 487 |
optionsHtml += cachedChallenges.map(c =>
|
| 488 |
-
`<option value="${c.id}">[${c.level}] ${c.title}</option>`
|
| 489 |
).join('');
|
| 490 |
}
|
| 491 |
|
| 492 |
return `
|
| 493 |
-
<div id="peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4">
|
| 494 |
<div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
|
| 495 |
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
|
| 496 |
<h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
|
|
@@ -507,7 +530,7 @@ function renderPeerModal() {
|
|
| 507 |
<div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
|
| 508 |
</div>
|
| 509 |
</div>
|
| 510 |
-
</div>
|
| 511 |
`;
|
| 512 |
}
|
| 513 |
|
|
@@ -543,7 +566,7 @@ window.loadPeerPrompts = async (challengeId) => {
|
|
| 543 |
const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
|
| 544 |
|
| 545 |
return `
|
| 546 |
-
<div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600">
|
| 547 |
<div class="flex items-center justify-between mb-2">
|
| 548 |
<div class="flex items-center space-x-2">
|
| 549 |
<div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
|
|
@@ -562,11 +585,11 @@ window.loadPeerPrompts = async (challengeId) => {
|
|
| 562 |
</button>
|
| 563 |
</div>
|
| 564 |
<p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
|
| 565 |
-
</div>
|
| 566 |
`}).join('');
|
| 567 |
|
| 568 |
// Attach challenge title for notification context
|
| 569 |
-
window.currentPeerChallengeTitle = document.querySelector(`#peer-challenge-select option[value="${challengeId}"]`).text;
|
| 570 |
};
|
| 571 |
|
| 572 |
// Like Handler
|
|
|
|
| 1 |
import { submitPrompt, getChallenges, startChallenge, getUserProgress, getPeerPrompts, resetProgress, toggleLike, subscribeToNotifications, markNotificationRead, getClassSize, updateUserMonster, getUser, subscribeToUserProgress } from "../services/classroom.js";
|
| 2 |
+
import { generateMonsterSVG, getNextMonster, MONSTER_STAGES, MONSTER_DEFS } from "../utils/monsterUtils.js";
|
| 3 |
|
| 4 |
|
| 5 |
// Cache challenges locally
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
return `
|
| 201 |
+
// Left sidebar position: Fixed Left-8 or similar.
|
| 202 |
+
// User wants it in the empty left area.
|
| 203 |
+
return `
|
| 204 |
+
< div id = "monster-container-fixed" class="fixed top-32 left-8 z-50 flex flex-col items-center group pointer-events-none sm:pointer-events-auto w-32 h-32" >
|
| 205 |
+
< !--Walking Container-- >
|
| 206 |
+
<div class="pixel-art-container relative transform transition-transform duration-500 ease-out origin-center"
|
| 207 |
style="transform: scale(${currentScale}); animation: walk-float 6s ease-in-out infinite;">
|
| 208 |
|
| 209 |
<div class="pixel-monster w-28 h-28 drop-shadow-2xl filter" style="animation: breathe 3s infinite ease-in-out;">
|
|
|
|
| 216 |
</div>
|
| 217 |
</div>
|
| 218 |
|
| 219 |
+
<!--Evolution Prompt-- >
|
| 220 |
+
${
|
| 221 |
+
canEvolve ? `
|
| 222 |
<div id="evolution-prompt" class="absolute top-full mt-2 pointer-events-auto animate-bounce z-50">
|
| 223 |
<div class="flex flex-col items-center">
|
| 224 |
<div class="bg-gray-900/90 text-pink-200 text-xs py-2 px-3 rounded-xl border border-pink-500/30 shadow-lg text-center font-bold mb-1 backdrop-blur-sm whitespace-nowrap">
|
|
|
|
| 230 |
</button>
|
| 231 |
</div>
|
| 232 |
</div>
|
| 233 |
+
` : ''
|
| 234 |
+
}
|
| 235 |
|
| 236 |
+
< !--Stats Tooltip-- >
|
| 237 |
<div class="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gray-900/90 backdrop-blur text-xs text-slate-300 p-3 rounded-xl border border-slate-700 mt-6 text-left pointer-events-auto shadow-2xl min-w-[120px]">
|
| 238 |
<div class="font-bold text-white text-sm mb-1 text-center border-b border-gray-700 pb-1">${monster.name}</div>
|
| 239 |
<div class="space-y-1 mt-1">
|
|
|
|
| 242 |
<div class="flex justify-between"><span>⚔️ 任務:</span> <span class="text-yellow-400 font-bold">${totalCompleted}</span></div>
|
| 243 |
</div>
|
| 244 |
</div>
|
| 245 |
+
</div >
|
| 246 |
|
| 247 |
<style>
|
| 248 |
@keyframes breathe {
|
| 249 |
+
0 %, 100 % { transform: translateY(0); filter: brightness(1); }
|
| 250 |
+
50% {transform: translateY(-3px); filter: brightness(1.1); }
|
| 251 |
}
|
| 252 |
@keyframes walk-float {
|
| 253 |
+
0 %, 100 % { transform: translateX(0) scale(${ currentScale }); }
|
| 254 |
+
25% {transform: translateX(-10px) rotate(-2deg) scale(${currentScale}); }
|
| 255 |
+
75% {transform: translateX(10px) rotate(2deg) scale(${currentScale}); }
|
| 256 |
}
|
| 257 |
</style>
|
| 258 |
+
`;
|
| 259 |
};
|
| 260 |
|
| 261 |
// Inject Initial Monster UI
|
|
|
|
| 292 |
|
| 293 |
// Accordion Layout
|
| 294 |
return `
|
| 295 |
+
< div class="min-h-screen p-4 pb-32 max-w-md mx-auto sm:max-w-4xl pt-24 sm:pt-4" >
|
| 296 |
<header class="flex justify-end items-center mb-6 sticky top-0 bg-slate-900/95 backdrop-blur z-20 py-4 px-2 -mx-2 border-b border-gray-800">
|
| 297 |
<div class="flex flex-col items-end">
|
| 298 |
<div class="flex items-center space-x-2">
|
|
|
|
| 310 |
<div class="space-y-4">
|
| 311 |
${['beginner', 'intermediate', 'advanced'].map(level => {
|
| 312 |
const tasks = levelGroups[level] || [];
|
| 313 |
+
|
| 314 |
+
// State Preservation Logic:
|
| 315 |
+
// Check DOM for existing details element and its open attribute
|
| 316 |
+
// ID strategy: details-{level}
|
| 317 |
+
// If not found, default to open for 'beginner', closed for others.
|
| 318 |
+
// But if we are re-rendering, we want to keep current state.
|
| 319 |
+
|
| 320 |
+
const detailsId = `details-group-${level}`;
|
| 321 |
+
const existingDetails = document.getElementById(detailsId);
|
| 322 |
+
let isOpenStr = '';
|
| 323 |
+
|
| 324 |
+
if (existingDetails) {
|
| 325 |
+
if (existingDetails.hasAttribute('open')) isOpenStr = 'open';
|
| 326 |
+
} else {
|
| 327 |
+
// Initial Load defaults
|
| 328 |
+
if (level === 'beginner') isOpenStr = 'open';
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
// Count completed
|
| 332 |
const completedCount = tasks.filter(t => userProgress[t.id]?.status === 'completed').length;
|
| 333 |
+
const allClear = completedCount === tasks.length && tasks.length > 0;
|
| 334 |
|
| 335 |
return `
|
| 336 |
+
<details id="${detailsId}" class="group border border-gray-700 rounded-xl bg-gray-800/30 overflow-hidden transition-all" ${isOpenStr}>
|
| 337 |
<summary class="flex items-center justify-between p-4 cursor-pointer bg-gray-800/80 hover:bg-gray-700 transition-colors select-none">
|
| 338 |
<div class="flex items-center space-x-3">
|
| 339 |
<h3 class="text-lg font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-cyan-400">
|
|
|
|
| 356 |
}).join('')}
|
| 357 |
</div>
|
| 358 |
|
| 359 |
+
<!--Peer Learning FAB-- >
|
| 360 |
<button onclick="window.openPeerModal()" class="fixed bottom-6 right-6 bg-purple-600 hover:bg-purple-500 text-white rounded-full p-4 shadow-xl shadow-purple-600/40 transition-transform hover:scale-110 active:scale-90 z-40"
|
| 361 |
title="查看同學作業">
|
| 362 |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 363 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0z" />
|
| 364 |
</svg>
|
| 365 |
</button>
|
| 366 |
+
</div >
|
| 367 |
+
`;
|
| 368 |
}
|
| 369 |
|
| 370 |
export function setupStudentEvents() {
|
|
|
|
| 394 |
};
|
| 395 |
|
| 396 |
window.submitLevel = async (challengeId) => {
|
| 397 |
+
const input = document.getElementById(`input - ${ challengeId } `);
|
| 398 |
+
const errorMsg = document.getElementById(`error - ${ challengeId } `);
|
| 399 |
const prompt = input.value;
|
| 400 |
const roomCode = localStorage.getItem('vibecoding_room_code');
|
| 401 |
const userId = localStorage.getItem('vibecoding_user_id');
|
|
|
|
| 441 |
const newCardHTML = renderTaskCard(challenge, newProgress);
|
| 442 |
|
| 443 |
// 3. Replace in DOM
|
| 444 |
+
const oldCard = document.getElementById(`card - ${ challengeId } `);
|
| 445 |
if (oldCard) {
|
| 446 |
oldCard.outerHTML = newCardHTML;
|
| 447 |
} else {
|
|
|
|
| 508 |
let optionsHtml = '<option value="" disabled selected>選擇題目...</option>';
|
| 509 |
if (cachedChallenges.length > 0) {
|
| 510 |
optionsHtml += cachedChallenges.map(c =>
|
| 511 |
+
`< option value = "${c.id}" > [${ c.level }] ${ c.title }</option > `
|
| 512 |
).join('');
|
| 513 |
}
|
| 514 |
|
| 515 |
return `
|
| 516 |
+
< div id = "peer-modal" class="fixed inset-0 bg-black bg-opacity-80 backdrop-blur-sm hidden flex items-center justify-center z-50 p-4" >
|
| 517 |
<div class="bg-gray-800 rounded-2xl w-full max-w-md h-[80vh] flex flex-col border border-gray-700 shadow-2xl">
|
| 518 |
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
|
| 519 |
<h3 class="text-xl font-bold text-white">同學的成功提示詞</h3>
|
|
|
|
| 530 |
<div class="text-center text-gray-500 mt-10">請選擇一個題目來查看</div>
|
| 531 |
</div>
|
| 532 |
</div>
|
| 533 |
+
</div >
|
| 534 |
`;
|
| 535 |
}
|
| 536 |
|
|
|
|
| 566 |
const isLiked = p.likedBy && p.likedBy.includes(currentUserId);
|
| 567 |
|
| 568 |
return `
|
| 569 |
+
< div class="bg-gray-700/30 p-4 rounded-xl border border-gray-600" >
|
| 570 |
<div class="flex items-center justify-between mb-2">
|
| 571 |
<div class="flex items-center space-x-2">
|
| 572 |
<div class="w-6 h-6 rounded-full bg-cyan-600 flex items-center justify-center text-xs font-bold text-white">
|
|
|
|
| 585 |
</button>
|
| 586 |
</div>
|
| 587 |
<p class="text-gray-300 font-mono text-sm bg-black/20 p-3 rounded-lg border border-gray-700/50 whitespace-pre-wrap">${p.prompt}</p>
|
| 588 |
+
</div >
|
| 589 |
`}).join('');
|
| 590 |
|
| 591 |
// Attach challenge title for notification context
|
| 592 |
+
window.currentPeerChallengeTitle = document.querySelector(`#peer - challenge - select option[value = "${challengeId}"]`).text;
|
| 593 |
};
|
| 594 |
|
| 595 |
// Like Handler
|