`;
}
export function setupStudentEvents() {
// Start Level Logic
window.startLevel = async (challengeId, link) => {
// Open link
window.open(link, '_blank');
// Call service to update status
const roomCode = localStorage.getItem('vibecoding_room_code');
const userId = localStorage.getItem('vibecoding_user_id');
if (roomCode && userId) {
try {
await startChallenge(userId, roomCode, challengeId);
// Reload view to show Input State
// Ideally we should use state management, but checking URL hash or re-rendering works
const app = document.querySelector('#app');
app.innerHTML = await renderStudentView();
// Re-attach events (recursion safety check needed? No, navigateTo does this usually, but here we manually re-render)
// Or better: trigger a custom event or call navigateTo functionality?
// Simple re-render is fine for now.
} catch (e) {
console.error("Start challenge failed", e);
}
}
};
window.submitLevel = async (challengeId) => {
const input = document.getElementById(`input-${challengeId}`);
const errorMsg = document.getElementById(`error-${challengeId}`);
const prompt = input.value;
const roomCode = localStorage.getItem('vibecoding_room_code');
const userId = localStorage.getItem('vibecoding_user_id');
if (!participantDataCheck(roomCode, userId)) return;
if (prompt.trim().length < 5) {
errorMsg.classList.remove('hidden');
input.classList.add('border-red-500');
return;
}
errorMsg.classList.add('hidden');
input.classList.remove('border-red-500');
// Show loading state on button
const container = input.parentElement;
const btn = container.querySelector('button');
const originalText = btn.textContent;
btn.textContent = "提交中...";
btn.disabled = true;
try {
await submitPrompt(userId, roomCode, challengeId, prompt);
btn.textContent = "✓ 已通關";
btn.classList.add("bg-green-600");
// NEW: Partial Update Strategy directly
// 1. Find the container
// The card is the great-great-grandparent of the button (button -> div -> div -> div -> card)
// Or simpler: give ID to card wrapper in renderTaskCard.
// Let's assume renderTaskCard now adds id="card-${c.id}"
// 2. Re-render just this card
const challenge = cachedChallenges.find(c => c.id === challengeId);
const newProgress = { [challengeId]: { status: 'completed', submission_prompt: prompt } };
// We need to merge with existing progress to pass to renderTaskCard?
// Actually renderTaskCard takes the whole userProgress map.
// But for this specific card, we can just pass a map containing just this one, because renderTaskCard(c, map) only looks up map[c.id].
const newCardHTML = renderTaskCard(challenge, newProgress);
// 3. Replace in DOM
const oldCard = document.getElementById(`card-${challengeId}`);
if (oldCard) {
oldCard.outerHTML = newCardHTML;
} else {
// Fallback if ID not found (should not happen if we update renderTaskCard)
const app = document.querySelector('#app');
app.innerHTML = await renderStudentView();
}
} catch (error) {
console.error(error);
btn.textContent = originalText;
btn.disabled = false;
alert("提交失敗: " + error.message);
}
};
window.resetLevel = async (challengeId) => {
if (!confirm("確定要重置這一題的進度嗎?(提示詞將會保留,但狀態會變回進行中)")) return;
const roomCode = localStorage.getItem('vibecoding_room_code');
const userId = localStorage.getItem('vibecoding_user_id');
try {
// Import and call resetProgress (Need to make sure it is imported or available globally?
// Ideally import it. But setupStudentEvents is in module scope so imports are available.
// Wait, import 'resetProgress' is not in the top import list yet. I need to add it.)
// Let's assume I will update the import in the next step or use the global trick if needed.
// But I should edit the import first.
// For now, let's assume it is there. I will add it to the import list in a parallel or subsequent edit.
// Checking imports above... I see 'getUserProgress' but not 'resetProgress'. I must update imports.
// I'll do it in a separate edit step to be safe.
// For now, just the logic:
const { resetProgress } = await import("../services/classroom.js"); // Dynamic import to avoid changing top file lines again?
// Or just rely on previous 'replace' having updated the file?
// Actually, I should update the top import.
await resetProgress(userId, roomCode, challengeId);
const app = document.querySelector('#app');
app.innerHTML = await renderStudentView();
} catch (e) {
console.error(e);
alert("重置失敗");
}
};
}
function participantDataCheck(roomCode, userId) {
if (!roomCode || !userId) {
alert("連線資訊遺失,請重新登入");
window.location.reload();
return false;
}
return true;
}
// Peer Learning Modal Logic
function renderPeerModal() {
// We need to re-fetch challenges for the dropdown?
// They are cached in 'cachedChallenges' module variable
let optionsHtml = '';
if (cachedChallenges.length > 0) {
optionsHtml += cachedChallenges.map(c =>
``
).join('');
}
return `
`}).join('');
// Attach challenge title for notification context
window.currentPeerChallengeTitle = document.querySelector(`#peer-challenge-select option[value="${challengeId}"]`).text;
};
// Like Handler
window.handleLike = async (progressId, targetUserId) => {
const userId = localStorage.getItem('vibecoding_user_id');
const nickname = localStorage.getItem('vibecoding_nickname');
const challengeTitle = window.currentPeerChallengeTitle || '挑戰';
// Optimistic UI update could go here, but for simplicity let's re-load or just fire and forget (the view won't update until reload currently)
// To make it responsive, we should probably manually toggle the class on the button immediately.
// For now, let's just call service and reload the list to see updated count.
// Better UX: Find button and toggle 'processing' state?
// Let's just reload the list for data consistency.
const { toggleLike } = await import("../services/classroom.js");
await toggleLike(progressId, userId, nickname, targetUserId, challengeTitle);
// Reload to refresh count
const select = document.getElementById('peer-challenge-select');
if (select && select.value) {
loadPeerPrompts(select.value);
}
};
window.triggerEvolution = async (currentStage, nextStage, likes, classSize) => {
// 1. Hide Prompt
const prompt = document.getElementById('evolution-prompt');
if (prompt) prompt.style.display = 'none';
// 2. Prepare Animation Data
// We need Next Monster Data
// We can't easily import logic here if not exposed, but we exported getNextMonster.
// We need to re-import or use the one in scope if available.
// Fortunately setupStudentEvents is a module, but this function is on window.
// We need to pass data or use a helper.
// Ideally we should move getNextMonster to a global helper or fetch it.
// Let's use dynamic import to be safe and robust.
try {
const { getNextMonster, generateMonsterSVG, MONSTER_STAGES } = await import("../utils/monsterUtils.js");
const { updateUserStage } = await import("../services/classroom.js");
const currentMonster = getNextMonster(currentStage, likes, classSize);
const nextMonster = getNextMonster(nextStage, likes, classSize);
const container = document.querySelector('#monster-container-fixed .pixel-monster');
const containerWrapper = document.querySelector('#monster-container-fixed .pixel-art-container');
// Stop breathing animation
container.style.animation = 'none';
// --- ANIMATION SEQUENCE ---
// flicker count
let count = 0;
const maxFlickers = 10;
let speed = 300; // start slow
const svgCurrent = generateMonsterSVG(currentMonster);
const svgNext = generateMonsterSVG(nextMonster);
// Helper to set Content and Style
const setFrame = (svg, isSilhouette) => {
container.innerHTML = svg;
container.style.filter = isSilhouette ? 'brightness(0) invert(1)' : 'none'; // White silhouette? User said 'silhouette' usually black or white. Let's try Black (brightness 0)
if (isSilhouette) container.style.filter = 'brightness(0)';
};
const playFlicker = () => {
// Alternate
const isNext = count % 2 === 1;
setFrame(isNext ? svgNext : svgCurrent, true);
count++;
if (count < maxFlickers) {
// Speed up
speed *= 0.8;
setTimeout(playFlicker, speed);
} else {
// Final Reveal
setTimeout(() => {
// Pause on Next Silhouette
setFrame(svgNext, true);
setTimeout(() => {
// Reveal Color with flash
containerWrapper.style.transition = 'filter 0.5s ease-out';
containerWrapper.style.filter = 'drop-shadow(0 0 20px #ffffff)'; // Flash
setFrame(svgNext, false); // Color
setTimeout(async () => {
containerWrapper.style.filter = 'none';
// DB Update
const userId = localStorage.getItem('vibecoding_user_id');
await updateUserStage(userId, nextStage);
// Reload
const app = document.querySelector('#app');
// We need to re-import renderStudentView? It's exported.
// But we are inside window function.
// Just generic reload for now or try to re-render if accessible.
// renderStudentView is not global.
// Let's reload page to be cleanest or rely on the subscribeToUserProgress which might flicker?
// subscribeToUserProgress listens to PROGRESS collection, not USERS collection (where monster scale is).
// So we MUST reload or manually fetch profile.
// Simple reload:
// window.location.reload();
// Or better: triggering the view re-render.
// Note: We don't have access to 'renderStudentView' function here easily unless we attached it to window.
// Let's reload to ensure clean state.
window.location.reload();
}, 1000);
}, 800);
}, 200);
}
};
playFlicker();
} catch (e) {
console.error(e);
alert("進化失敗...");
window.location.reload();
}
};